├── .gitignore
├── Readme.md
├── deployments
├── kustomization.yaml
├── mysql-deployment.yaml
├── polling-app-client.yaml
└── polling-app-server.yaml
├── docker-compose.yml
├── minikube.txt
├── polling-app-client
├── .dockerignore
├── .gitignore
├── Dockerfile
├── README.md
├── config-overrides.js
├── nginx.conf
├── package-lock.json
├── package.json
├── public
│ ├── favicon.png
│ ├── index.html
│ └── manifest.json
├── src
│ ├── app
│ │ ├── App.css
│ │ └── App.js
│ ├── common
│ │ ├── AppHeader.css
│ │ ├── AppHeader.js
│ │ ├── LoadingIndicator.js
│ │ ├── NotFound.css
│ │ ├── NotFound.js
│ │ ├── PrivateRoute.js
│ │ ├── ServerError.css
│ │ └── ServerError.js
│ ├── constants
│ │ └── index.js
│ ├── index.css
│ ├── index.js
│ ├── logo.svg
│ ├── poll.svg
│ ├── poll
│ │ ├── NewPoll.css
│ │ ├── NewPoll.js
│ │ ├── Poll.css
│ │ ├── Poll.js
│ │ ├── PollList.css
│ │ └── PollList.js
│ ├── registerServiceWorker.js
│ ├── user
│ │ ├── login
│ │ │ ├── Login.css
│ │ │ └── Login.js
│ │ ├── profile
│ │ │ ├── Profile.css
│ │ │ └── Profile.js
│ │ └── signup
│ │ │ ├── Signup.css
│ │ │ └── Signup.js
│ └── util
│ │ ├── APIUtils.js
│ │ ├── Colors.js
│ │ └── Helpers.js
└── yarn.lock
├── polling-app-server
├── .gitignore
├── .mvn
│ └── wrapper
│ │ ├── maven-wrapper.jar
│ │ └── maven-wrapper.properties
├── Dockerfile
├── mvnw
├── mvnw.cmd
├── pom.xml
└── src
│ ├── main
│ ├── java
│ │ └── com
│ │ │ └── example
│ │ │ └── polls
│ │ │ ├── PollsApplication.java
│ │ │ ├── config
│ │ │ ├── AuditingConfig.java
│ │ │ ├── SecurityConfig.java
│ │ │ └── WebMvcConfig.java
│ │ │ ├── controller
│ │ │ ├── AuthController.java
│ │ │ ├── PollController.java
│ │ │ └── UserController.java
│ │ │ ├── exception
│ │ │ ├── AppException.java
│ │ │ ├── BadRequestException.java
│ │ │ └── ResourceNotFoundException.java
│ │ │ ├── model
│ │ │ ├── Choice.java
│ │ │ ├── ChoiceVoteCount.java
│ │ │ ├── Poll.java
│ │ │ ├── Role.java
│ │ │ ├── RoleName.java
│ │ │ ├── User.java
│ │ │ ├── Vote.java
│ │ │ └── audit
│ │ │ │ ├── DateAudit.java
│ │ │ │ └── UserDateAudit.java
│ │ │ ├── payload
│ │ │ ├── ApiResponse.java
│ │ │ ├── ChoiceRequest.java
│ │ │ ├── ChoiceResponse.java
│ │ │ ├── JwtAuthenticationResponse.java
│ │ │ ├── LoginRequest.java
│ │ │ ├── PagedResponse.java
│ │ │ ├── PollLength.java
│ │ │ ├── PollRequest.java
│ │ │ ├── PollResponse.java
│ │ │ ├── SignUpRequest.java
│ │ │ ├── UserIdentityAvailability.java
│ │ │ ├── UserProfile.java
│ │ │ ├── UserSummary.java
│ │ │ └── VoteRequest.java
│ │ │ ├── repository
│ │ │ ├── PollRepository.java
│ │ │ ├── RoleRepository.java
│ │ │ ├── UserRepository.java
│ │ │ └── VoteRepository.java
│ │ │ ├── security
│ │ │ ├── CurrentUser.java
│ │ │ ├── CustomUserDetailsService.java
│ │ │ ├── JwtAuthenticationEntryPoint.java
│ │ │ ├── JwtAuthenticationFilter.java
│ │ │ ├── JwtTokenProvider.java
│ │ │ └── UserPrincipal.java
│ │ │ ├── service
│ │ │ └── PollService.java
│ │ │ └── util
│ │ │ ├── AppConstants.java
│ │ │ └── ModelMapper.java
│ └── resources
│ │ ├── application.properties
│ │ ├── data.sql
│ │ └── db
│ │ └── migration
│ │ ├── V1__schema.sql
│ │ └── V2__default_roles.sql
│ └── test
│ └── java
│ └── com
│ └── example
│ └── polls
│ └── PollsApplicationTests.java
└── screenshot.png
/.gitignore:
--------------------------------------------------------------------------------
1 | .DS_Store
2 |
--------------------------------------------------------------------------------
/Readme.md:
--------------------------------------------------------------------------------
1 | ## Building a Full Stack Polls app similar to twitter polls with Spring Boot, Spring Security, JWT, React and Ant Design
2 |
3 | 
4 |
5 | ### Tutorials
6 |
7 | I've written a complete tutorial series for this application on The CalliCoder Blog -
8 |
9 | + [Part 1: Bootstrapping the Project and creating the basic domain models and repositories](https://www.callicoder.com/spring-boot-spring-security-jwt-mysql-react-app-part-1/)
10 |
11 | + [Part 2: Configuring Spring Security along with JWT authentication and Building Rest APIs for Login and SignUp](https://www.callicoder.com/spring-boot-spring-security-jwt-mysql-react-app-part-2/)
12 |
13 | + [Part 3: Building Rest APIs for creating Polls, voting for a choice in a Poll, retrieving user profile etc](https://www.callicoder.com/spring-boot-spring-security-jwt-mysql-react-app-part-3/)
14 |
15 | + [Part 4: Building the front-end using React and Ant Design](https://www.callicoder.com/spring-boot-spring-security-jwt-mysql-react-app-part-4/)
16 |
17 | ## Steps to Setup the Spring Boot Back end app (polling-app-server)
18 |
19 | 1. **Clone the application**
20 |
21 | ```bash
22 | git clone https://github.com/callicoder/spring-security-react-ant-design-polls-app.git
23 | cd polling-app-server
24 | ```
25 |
26 | 2. **Create MySQL database**
27 |
28 | ```bash
29 | create database polling_app
30 | ```
31 |
32 | 3. **Change MySQL username and password as per your MySQL installation**
33 |
34 | + open `src/main/resources/application.properties` file.
35 |
36 | + change `spring.datasource.username` and `spring.datasource.password` properties as per your mysql installation
37 |
38 | 4. **Run the app**
39 |
40 | You can run the spring boot app by typing the following command -
41 |
42 | ```bash
43 | mvn spring-boot:run
44 | ```
45 |
46 | The server will start on port 8080.
47 |
48 | You can also package the application in the form of a `jar` file and then run it like so -
49 |
50 | ```bash
51 | mvn package
52 | java -jar target/polls-0.0.1-SNAPSHOT.jar
53 | ```
54 | 5. **Default Roles**
55 |
56 | The spring boot app uses role based authorization powered by spring security. To add the default roles in the database, I have added the following sql queries in `src/main/resources/data.sql` file. Spring boot will automatically execute this script on startup -
57 |
58 | ```sql
59 | INSERT IGNORE INTO roles(name) VALUES('ROLE_USER');
60 | INSERT IGNORE INTO roles(name) VALUES('ROLE_ADMIN');
61 | ```
62 |
63 | Any new user who signs up to the app is assigned the `ROLE_USER` by default.
64 |
65 | ## Steps to Setup the React Front end app (polling-app-client)
66 |
67 | First go to the `polling-app-client` folder -
68 |
69 | ```bash
70 | cd polling-app-client
71 | ```
72 |
73 | Then type the following command to install the dependencies and start the application -
74 |
75 | ```bash
76 | npm install && npm start
77 | ```
78 |
79 | The front-end server will start on port `3000`.
80 |
--------------------------------------------------------------------------------
/deployments/kustomization.yaml:
--------------------------------------------------------------------------------
1 | # The secrets file should not be checked into Git. It's published only for demonstration purpose.
2 | secretGenerator:
3 | - name: mysql-root-pass
4 | literals:
5 | - password=R00t
6 | - name: mysql-user-pass
7 | literals:
8 | - username=callicoder
9 | - password=c@ll1c0d3r
10 | - name: mysql-db-url
11 | literals:
12 | - database=polls
13 | - url=jdbc:mysql://polling-app-mysql:3306/polls?useSSL=false&serverTimezone=UTC&useLegacyDatetimeCode=false
14 | resources:
15 | - mysql-deployment.yaml
16 | - polling-app-server.yaml
17 | - polling-app-client.yaml
18 |
--------------------------------------------------------------------------------
/deployments/mysql-deployment.yaml:
--------------------------------------------------------------------------------
1 | apiVersion: v1
2 | kind: PersistentVolume # Create a PersistentVolume
3 | metadata:
4 | name: mysql-pv
5 | labels:
6 | type: local
7 | spec:
8 | storageClassName: standard # Storage class. A PV Claim requesting the same storageClass can be bound to this volume.
9 | capacity:
10 | storage: 250Mi
11 | accessModes:
12 | - ReadWriteOnce
13 | hostPath: # hostPath PersistentVolume is used for development and testing. It uses a file/directory on the Node to emulate network-attached storage
14 | path: "/mnt/data"
15 | persistentVolumeReclaimPolicy: Retain # Retain the PersistentVolume even after PersistentVolumeClaim is deleted. The volume is considered “released”. But it is not yet available for another claim because the previous claimant’s data remains on the volume.
16 | ---
17 | apiVersion: v1
18 | kind: PersistentVolumeClaim # Create a PersistentVolumeClaim to request a PersistentVolume storage
19 | metadata: # Claim name and labels
20 | name: mysql-pv-claim
21 | labels:
22 | app: polling-app
23 | spec: # Access mode and resource limits
24 | storageClassName: standard # Request a certain storage class
25 | accessModes:
26 | - ReadWriteOnce # ReadWriteOnce means the volume can be mounted as read-write by a single Node
27 | resources:
28 | requests:
29 | storage: 250Mi
30 | ---
31 | apiVersion: v1 # API version
32 | kind: Service # Type of kubernetes resource
33 | metadata:
34 | name: polling-app-mysql # Name of the resource
35 | labels: # Labels that will be applied to the resource
36 | app: polling-app
37 | spec:
38 | ports:
39 | - port: 3306
40 | selector: # Selects any Pod with labels `app=polling-app,tier=mysql`
41 | app: polling-app
42 | tier: mysql
43 | clusterIP: None
44 | ---
45 | apiVersion: apps/v1
46 | kind: Deployment # Type of the kubernetes resource
47 | metadata:
48 | name: polling-app-mysql # Name of the deployment
49 | labels: # Labels applied to this deployment
50 | app: polling-app
51 | spec:
52 | selector:
53 | matchLabels: # This deployment applies to the Pods matching the specified labels
54 | app: polling-app
55 | tier: mysql
56 | strategy:
57 | type: Recreate
58 | template: # Template for the Pods in this deployment
59 | metadata:
60 | labels: # Labels to be applied to the Pods in this deployment
61 | app: polling-app
62 | tier: mysql
63 | spec: # The spec for the containers that will be run inside the Pods in this deployment
64 | containers:
65 | - image: mysql:5.6 # The container image
66 | name: mysql
67 | env: # Environment variables passed to the container
68 | - name: MYSQL_ROOT_PASSWORD
69 | valueFrom: # Read environment variables from kubernetes secrets
70 | secretKeyRef:
71 | name: mysql-root-pass
72 | key: password
73 | - name: MYSQL_DATABASE
74 | valueFrom:
75 | secretKeyRef:
76 | name: mysql-db-url
77 | key: database
78 | - name: MYSQL_USER
79 | valueFrom:
80 | secretKeyRef:
81 | name: mysql-user-pass
82 | key: username
83 | - name: MYSQL_PASSWORD
84 | valueFrom:
85 | secretKeyRef:
86 | name: mysql-user-pass
87 | key: password
88 | ports:
89 | - containerPort: 3306 # The port that the container exposes
90 | name: mysql
91 | volumeMounts:
92 | - name: mysql-persistent-storage # This name should match the name specified in `volumes.name`
93 | mountPath: /var/lib/mysql
94 | volumes: # A PersistentVolume is mounted as a volume to the Pod
95 | - name: mysql-persistent-storage
96 | persistentVolumeClaim:
97 | claimName: mysql-pv-claim
--------------------------------------------------------------------------------
/deployments/polling-app-client.yaml:
--------------------------------------------------------------------------------
1 | apiVersion: apps/v1 # API version
2 | kind: Deployment # Type of kubernetes resource
3 | metadata:
4 | name: polling-app-client # Name of the kubernetes resource
5 | spec:
6 | replicas: 1 # No of replicas/pods to run
7 | selector:
8 | matchLabels: # This deployment applies to Pods matching the specified labels
9 | app: polling-app-client
10 | template: # Template for creating the Pods in this deployment
11 | metadata:
12 | labels: # Labels that will be applied to all the Pods in this deployment
13 | app: polling-app-client
14 | spec: # Spec for the containers that will run inside the Pods
15 | containers:
16 | - name: polling-app-client
17 | image: callicoder/polling-app-client:1.0.0
18 | imagePullPolicy: IfNotPresent
19 | ports:
20 | - name: http
21 | containerPort: 80 # Should match the Port that the container listens on
22 | resources:
23 | limits:
24 | cpu: 0.2
25 | memory: "10Mi"
26 | ---
27 | apiVersion: v1 # API version
28 | kind: Service # Type of kubernetes resource
29 | metadata:
30 | name: polling-app-client # Name of the kubernetes resource
31 | spec:
32 | type: NodePort # Exposes the service by opening a port on each node
33 | selector:
34 | app: polling-app-client # Any Pod matching the label `app=polling-app-client` will be picked up by this service
35 | ports: # Forward incoming connections on port 80 to the target port 80 in the Pod
36 | - name: http
37 | port: 80
38 | targetPort: 80
--------------------------------------------------------------------------------
/deployments/polling-app-server.yaml:
--------------------------------------------------------------------------------
1 | apiVersion: apps/v1 # API version
2 | kind: Deployment # Type of kubernetes resource
3 | metadata:
4 | name: polling-app-server # Name of the kubernetes resource
5 | labels: # Labels that will be applied to this resource
6 | app: polling-app-server
7 | spec:
8 | replicas: 1 # No. of replicas/pods to run in this deployment
9 | selector:
10 | matchLabels: # The deployment applies to any pods mayching the specified labels
11 | app: polling-app-server
12 | template: # Template for creating the pods in this deployment
13 | metadata:
14 | labels: # Labels that will be applied to each Pod in this deployment
15 | app: polling-app-server
16 | spec: # Spec for the containers that will be run in the Pods
17 | containers:
18 | - name: polling-app-server
19 | image: callicoder/polling-app-server:1.0.0
20 | imagePullPolicy: IfNotPresent
21 | ports:
22 | - name: http
23 | containerPort: 8080 # The port that the container exposes
24 | resources:
25 | limits:
26 | cpu: 0.2
27 | memory: "200Mi"
28 | env: # Environment variables supplied to the Pod
29 | - name: SPRING_DATASOURCE_USERNAME # Name of the environment variable
30 | valueFrom: # Get the value of environment variable from kubernetes secrets
31 | secretKeyRef:
32 | name: mysql-user-pass
33 | key: username
34 | - name: SPRING_DATASOURCE_PASSWORD
35 | valueFrom:
36 | secretKeyRef:
37 | name: mysql-user-pass
38 | key: password
39 | - name: SPRING_DATASOURCE_URL
40 | valueFrom:
41 | secretKeyRef:
42 | name: mysql-db-url
43 | key: url
44 | ---
45 | apiVersion: v1 # API version
46 | kind: Service # Type of the kubernetes resource
47 | metadata:
48 | name: polling-app-server # Name of the kubernetes resource
49 | labels: # Labels that will be applied to this resource
50 | app: polling-app-server
51 | spec:
52 | type: NodePort # The service will be exposed by opening a Port on each node and proxying it.
53 | selector:
54 | app: polling-app-server # The service exposes Pods with label `app=polling-app-server`
55 | ports: # Forward incoming connections on port 8080 to the target port 8080
56 | - name: http
57 | port: 8080
58 | targetPort: 8080
59 |
--------------------------------------------------------------------------------
/docker-compose.yml:
--------------------------------------------------------------------------------
1 | # Docker Compose file Reference (https://docs.docker.com/compose/compose-file/)
2 |
3 | version: '3.7'
4 |
5 | # Define services
6 | services:
7 | # App backend service
8 | app-server:
9 | # Configuration for building the docker image for the backend service
10 | build:
11 | context: polling-app-server # Use an image built from the specified dockerfile in the `polling-app-server` directory.
12 | dockerfile: Dockerfile
13 | ports:
14 | - "8080:8080" # Forward the exposed port 8080 on the container to port 8080 on the host machine
15 | restart: always
16 | depends_on:
17 | - db # This service depends on mysql. Start that first.
18 | environment: # Pass environment variables to the service
19 | SPRING_DATASOURCE_URL: jdbc:mysql://db:3306/polls?useSSL=false&serverTimezone=UTC&useLegacyDatetimeCode=false
20 | SPRING_DATASOURCE_USERNAME: callicoder
21 | SPRING_DATASOURCE_PASSWORD: callicoder
22 | networks: # Networks to join (Services on the same network can communicate with each other using their name)
23 | - backend
24 | - frontend
25 |
26 | # Frontend Service
27 | app-client:
28 | build:
29 | context: polling-app-client # Use an image built from the specified dockerfile in the `polling-app-client` directory.
30 | dockerfile: Dockerfile
31 | args:
32 | REACT_APP_API_BASE_URL: http://127.0.0.1:8080/api
33 | ports:
34 | - "9090:80" # Forward the exposed port 80 on the container to port 80 on the host machine
35 | restart: always
36 | depends_on:
37 | - app-server
38 | networks:
39 | - frontend
40 |
41 | # Database Service (Mysql)
42 | db:
43 | image: mysql:5.7
44 | ports:
45 | - "3306:3306"
46 | restart: always
47 | environment:
48 | MYSQL_DATABASE: polls
49 | MYSQL_USER: callicoder
50 | MYSQL_PASSWORD: callicoder
51 | MYSQL_ROOT_PASSWORD: root
52 | volumes:
53 | - db-data:/var/lib/mysql
54 | networks:
55 | - backend
56 |
57 | # Volumes
58 | volumes:
59 | db-data:
60 |
61 | # Networks to be created to facilitate communication between containers
62 | networks:
63 | backend:
64 | frontend:
--------------------------------------------------------------------------------
/minikube.txt:
--------------------------------------------------------------------------------
1 | $ minikube list
2 | $ kubectl port-forward service/polling-app-server 8080:8080
--------------------------------------------------------------------------------
/polling-app-client/.dockerignore:
--------------------------------------------------------------------------------
1 | .git
2 | node_modules
3 | build
4 |
--------------------------------------------------------------------------------
/polling-app-client/.gitignore:
--------------------------------------------------------------------------------
1 | # See https://help.github.com/ignore-files/ for more about ignoring files.
2 |
3 | # dependencies
4 | /node_modules
5 |
6 | # testing
7 | /coverage
8 |
9 | # production
10 | /build
11 |
12 | # misc
13 | .DS_Store
14 | .env.local
15 | .env.development.local
16 | .env.test.local
17 | .env.production.local
18 |
19 | npm-debug.log*
20 | yarn-debug.log*
21 | yarn-error.log*
22 |
--------------------------------------------------------------------------------
/polling-app-client/Dockerfile:
--------------------------------------------------------------------------------
1 | #### Stage 1: Build the react application
2 | FROM node:12.4.0-alpine as build
3 |
4 | # Configure the main working directory inside the docker image.
5 | # This is the base directory used in any further RUN, COPY, and ENTRYPOINT
6 | # commands.
7 | WORKDIR /app
8 |
9 | # Copy the package.json as well as the package-lock.json and install
10 | # the dependencies. This is a separate step so the dependencies
11 | # will be cached unless changes to one of those two files
12 | # are made.
13 | COPY package.json package-lock.json ./
14 | RUN npm install
15 |
16 | # Copy the main application
17 | COPY . ./
18 |
19 | # Arguments
20 | ARG REACT_APP_API_BASE_URL
21 | ENV REACT_APP_API_BASE_URL=${REACT_APP_API_BASE_URL}
22 |
23 | # Build the application
24 | RUN npm run build
25 |
26 | #### Stage 2: Serve the React application from Nginx
27 | FROM nginx:1.17.0-alpine
28 |
29 | # Copy the react build from Stage 1
30 | COPY --from=build /app/build /var/www
31 |
32 | # Copy our custom nginx config
33 | COPY nginx.conf /etc/nginx/nginx.conf
34 |
35 | # Expose port 3000 to the Docker host, so we can access it
36 | # from the outside.
37 | EXPOSE 80
38 |
39 | ENTRYPOINT ["nginx","-g","daemon off;"]
40 |
--------------------------------------------------------------------------------
/polling-app-client/config-overrides.js:
--------------------------------------------------------------------------------
1 | const { injectBabelPlugin } = require('react-app-rewired');
2 | const rewireLess = require('react-app-rewire-less');
3 |
4 | module.exports = function override(config, env) {
5 | config = injectBabelPlugin(['import', { libraryName: 'antd', style: true }], config);
6 | config = rewireLess.withLoaderOptions({
7 | modifyVars: {
8 | "@layout-body-background": "#FFFFFF",
9 | "@layout-header-background": "#FFFFFF",
10 | "@layout-footer-background": "#FFFFFF"
11 | },
12 | javascriptEnabled: true
13 | })(config, env);
14 | return config;
15 | };
--------------------------------------------------------------------------------
/polling-app-client/nginx.conf:
--------------------------------------------------------------------------------
1 | # auto detects a good number of processes to run
2 | worker_processes auto;
3 |
4 | #Provides the configuration file context in which the directives that affect connection processing are specified.
5 | events {
6 | # Sets the maximum number of simultaneous connections that can be opened by a worker process.
7 | worker_connections 8000;
8 | # Tells the worker to accept multiple connections at a time
9 | multi_accept on;
10 | }
11 |
12 |
13 | http {
14 | # what times to include
15 | include /etc/nginx/mime.types;
16 | # what is the default one
17 | default_type application/octet-stream;
18 |
19 | # Sets the path, format, and configuration for a buffered log write
20 | log_format compression '$remote_addr - $remote_user [$time_local] '
21 | '"$request" $status $upstream_addr '
22 | '"$http_referer" "$http_user_agent"';
23 |
24 | server {
25 | # listen on port 80
26 | listen 80;
27 |
28 | # save logs here
29 | access_log /var/log/nginx/access.log compression;
30 |
31 | # nginx root directory
32 | root /var/www;
33 |
34 | # what file to server as index
35 | index index.html index.htm;
36 |
37 | location / {
38 | # First attempt to serve request as file, then
39 | # as directory, then fall back to redirecting to index.html
40 | try_files $uri $uri/ /index.html;
41 | }
42 |
43 | # Media: images, icons, video, audio, HTC
44 | location ~* \.(?:jpg|jpeg|gif|png|ico|cur|gz|svg|svgz|mp4|ogg|ogv|webm|htc)$ {
45 | expires 1M;
46 | access_log off;
47 | add_header Cache-Control "public";
48 | }
49 |
50 | # Javascript and CSS files
51 | location ~* \.(?:css|js)$ {
52 | try_files $uri =404;
53 | expires 1y;
54 | access_log off;
55 | add_header Cache-Control "public";
56 | }
57 |
58 | # Any route containing a file extension (e.g. /devicesfile.js)
59 | location ~ ^.+\..+$ {
60 | try_files $uri =404;
61 | }
62 | }
63 | }
--------------------------------------------------------------------------------
/polling-app-client/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "polling-app-client",
3 | "version": "0.1.0",
4 | "private": true,
5 | "dependencies": {
6 | "antd": "^3.2.2",
7 | "react": "^16.5.2",
8 | "react-dom": "^16.5.2",
9 | "react-router-dom": "^4.3.1",
10 | "react-scripts": "1.1.5"
11 | },
12 | "scripts": {
13 | "start": "react-app-rewired start",
14 | "build": "react-app-rewired build",
15 | "test": "react-app-rewired test --env=jsdom",
16 | "eject": "react-scripts eject"
17 | },
18 | "devDependencies": {
19 | "babel-plugin-import": "^1.6.5",
20 | "react-app-rewire-less": "^2.1.0",
21 | "react-app-rewired": "^1.4.1"
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/polling-app-client/public/favicon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/callicoder/spring-security-react-ant-design-polls-app/362fad90cab17e76453b3b9e273c594de6ee3d7f/polling-app-client/public/favicon.png
--------------------------------------------------------------------------------
/polling-app-client/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
11 |
12 |
13 |
22 | Polling App | CalliCoder
23 |
24 |
25 |
26 |
27 | You need to enable JavaScript to run this app.
28 |
29 |
30 |
40 |
41 |
42 |
--------------------------------------------------------------------------------
/polling-app-client/public/manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "short_name": "React App",
3 | "name": "Create React App Sample",
4 | "icons": [
5 | {
6 | "src": "favicon.ico",
7 | "sizes": "64x64 32x32 24x24 16x16",
8 | "type": "image/x-icon"
9 | }
10 | ],
11 | "start_url": "./index.html",
12 | "display": "standalone",
13 | "theme_color": "#000000",
14 | "background_color": "#ffffff"
15 | }
16 |
--------------------------------------------------------------------------------
/polling-app-client/src/app/App.css:
--------------------------------------------------------------------------------
1 | .app-content {
2 | margin-top: 64px;
3 | }
--------------------------------------------------------------------------------
/polling-app-client/src/app/App.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react';
2 | import './App.css';
3 | import {
4 | Route,
5 | withRouter,
6 | Switch
7 | } from 'react-router-dom';
8 |
9 | import { getCurrentUser } from '../util/APIUtils';
10 | import { ACCESS_TOKEN } from '../constants';
11 |
12 | import PollList from '../poll/PollList';
13 | import NewPoll from '../poll/NewPoll';
14 | import Login from '../user/login/Login';
15 | import Signup from '../user/signup/Signup';
16 | import Profile from '../user/profile/Profile';
17 | import AppHeader from '../common/AppHeader';
18 | import NotFound from '../common/NotFound';
19 | import LoadingIndicator from '../common/LoadingIndicator';
20 | import PrivateRoute from '../common/PrivateRoute';
21 |
22 | import { Layout, notification } from 'antd';
23 | const { Content } = Layout;
24 |
25 | class App extends Component {
26 | constructor(props) {
27 | super(props);
28 | this.state = {
29 | currentUser: null,
30 | isAuthenticated: false,
31 | isLoading: true
32 | }
33 | this.handleLogout = this.handleLogout.bind(this);
34 | this.loadCurrentUser = this.loadCurrentUser.bind(this);
35 | this.handleLogin = this.handleLogin.bind(this);
36 |
37 | notification.config({
38 | placement: 'topRight',
39 | top: 70,
40 | duration: 3,
41 | });
42 | }
43 |
44 | loadCurrentUser() {
45 | getCurrentUser()
46 | .then(response => {
47 | this.setState({
48 | currentUser: response,
49 | isAuthenticated: true,
50 | isLoading: false
51 | });
52 | }).catch(error => {
53 | this.setState({
54 | isLoading: false
55 | });
56 | });
57 | }
58 |
59 | componentDidMount() {
60 | this.loadCurrentUser();
61 | }
62 |
63 | handleLogout(redirectTo="/", notificationType="success", description="You're successfully logged out.") {
64 | localStorage.removeItem(ACCESS_TOKEN);
65 |
66 | this.setState({
67 | currentUser: null,
68 | isAuthenticated: false
69 | });
70 |
71 | this.props.history.push(redirectTo);
72 |
73 | notification[notificationType]({
74 | message: 'Polling App',
75 | description: description,
76 | });
77 | }
78 |
79 | handleLogin() {
80 | notification.success({
81 | message: 'Polling App',
82 | description: "You're successfully logged in.",
83 | });
84 | this.loadCurrentUser();
85 | this.props.history.push("/");
86 | }
87 |
88 | render() {
89 | if(this.state.isLoading) {
90 | return
91 | }
92 |
93 | return (
94 |
95 |
98 |
99 |
100 |
101 |
102 | }>
105 |
106 | }>
108 |
109 | }>
111 |
112 |
113 |
114 |
115 |
116 |
117 |
118 | );
119 | }
120 | }
121 |
122 | export default withRouter(App);
123 |
--------------------------------------------------------------------------------
/polling-app-client/src/common/AppHeader.css:
--------------------------------------------------------------------------------
1 | .app-title {
2 | float: left;
3 | }
4 |
5 | .app-title a {
6 | text-decoration: none;
7 | line-height: 64px;
8 | font-size: 21px;
9 | display: inline-block;
10 | }
11 |
12 | .app-title a:hover {
13 | text-decoration: none;
14 | }
15 |
16 | .app-header {
17 | position: fixed;
18 | width: 100%;
19 | box-shadow: 0 2px 8px #f0f1f2;
20 | z-index: 10;
21 | padding: 0;
22 | }
23 |
24 | .app-menu {
25 | float: right;
26 | }
27 |
28 | .app-menu > li {
29 | padding: 0 20px;
30 | }
31 |
32 | .app-menu > li > a {
33 | padding: 0 20px;
34 | margin: 0 -20px;
35 | }
36 |
37 | .app-menu > li > a > i {
38 | margin-right: 0 !important;
39 | }
40 |
41 | .profile-dropdown-menu {
42 | min-width: 180px;
43 | }
44 |
45 | .profile-menu .user-full-name-info {
46 | font-size: 17px;
47 | font-weight: 600;
48 | color: rgba(0,0,0,0.85);
49 | }
50 |
51 | .profile-menu .username-info {
52 | font-size: 14px;
53 | color: rgba(0,0,0,0.65);
54 | }
55 |
56 | .dropdown-item {
57 | padding: 10px 12px;
58 | }
59 |
60 | .dropdown-item a {
61 | padding: 10px 12px;
62 | margin: -10px -12px;
63 | }
64 |
65 | .nav-icon {
66 | font-size: 20px;
67 | }
68 |
69 | .poll-icon {
70 | margin-top: -4px;
71 | }
72 |
73 | @media (max-width: 768px) {
74 | .app-title a {
75 | font-size: 20px;
76 | }
77 |
78 | .app-menu > li {
79 | padding: 0 15px;
80 | }
81 |
82 | .app-menu > li > a {
83 | padding: 0 15px;
84 | margin: 0 -15px;
85 | }
86 | }
--------------------------------------------------------------------------------
/polling-app-client/src/common/AppHeader.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react';
2 | import {
3 | Link,
4 | withRouter
5 | } from 'react-router-dom';
6 | import './AppHeader.css';
7 | import pollIcon from '../poll.svg';
8 | import { Layout, Menu, Dropdown, Icon } from 'antd';
9 | const Header = Layout.Header;
10 |
11 | class AppHeader extends Component {
12 | constructor(props) {
13 | super(props);
14 | this.handleMenuClick = this.handleMenuClick.bind(this);
15 | }
16 |
17 | handleMenuClick({ key }) {
18 | if(key === "logout") {
19 | this.props.onLogout();
20 | }
21 | }
22 |
23 | render() {
24 | let menuItems;
25 | if(this.props.currentUser) {
26 | menuItems = [
27 |
28 |
29 |
30 |
31 | ,
32 |
33 |
34 |
35 |
36 | ,
37 |
38 |
41 |
42 | ];
43 | } else {
44 | menuItems = [
45 |
46 | Login
47 | ,
48 |
49 | Signup
50 |
51 | ];
52 | }
53 |
54 | return (
55 |
56 |
57 |
58 | Polling App
59 |
60 |
65 | {menuItems}
66 |
67 |
68 |
69 | );
70 | }
71 | }
72 |
73 | function ProfileDropdownMenu(props) {
74 | const dropdownMenu = (
75 |
76 |
77 |
78 | {props.currentUser.name}
79 |
80 |
81 | @{props.currentUser.username}
82 |
83 |
84 |
85 |
86 | Profile
87 |
88 |
89 | Logout
90 |
91 |
92 | );
93 |
94 | return (
95 | document.getElementsByClassName('profile-menu')[0]}>
99 |
100 |
101 |
102 |
103 | );
104 | }
105 |
106 |
107 | export default withRouter(AppHeader);
--------------------------------------------------------------------------------
/polling-app-client/src/common/LoadingIndicator.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Spin, Icon } from 'antd';
3 |
4 | export default function LoadingIndicator(props) {
5 | const antIcon = ;
6 | return (
7 |
8 | );
9 | }
--------------------------------------------------------------------------------
/polling-app-client/src/common/NotFound.css:
--------------------------------------------------------------------------------
1 | .page-not-found {
2 | max-width: 500px;
3 | margin: 0 auto;
4 | margin-top: 50px;
5 | padding: 40px;
6 | border: 1px solid #c8c8c8;
7 | text-align: center;
8 | }
9 |
10 | .page-not-found .title {
11 | font-size: 50px;
12 | letter-spacing: 10px;
13 | margin-bottom: 10px;
14 | }
15 |
16 | .page-not-found .desc {
17 | font-size: 20px;
18 | margin-bottom: 20px;
19 | }
20 |
21 | .go-back-btn {
22 | min-width: 160px;
23 | }
--------------------------------------------------------------------------------
/polling-app-client/src/common/NotFound.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react';
2 | import './NotFound.css';
3 | import { Link } from 'react-router-dom';
4 | import { Button } from 'antd';
5 |
6 | class NotFound extends Component {
7 | render() {
8 | return (
9 |
10 |
11 | 404
12 |
13 |
14 | The Page you're looking for was not found.
15 |
16 |
Go Back
17 |
18 | );
19 | }
20 | }
21 |
22 | export default NotFound;
--------------------------------------------------------------------------------
/polling-app-client/src/common/PrivateRoute.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import {
3 | Route,
4 | Redirect
5 | } from "react-router-dom";
6 |
7 |
8 | const PrivateRoute = ({ component: Component, authenticated, ...rest }) => (
9 |
12 | authenticated ? (
13 |
14 | ) : (
15 |
21 | )
22 | }
23 | />
24 | );
25 |
26 | export default PrivateRoute
--------------------------------------------------------------------------------
/polling-app-client/src/common/ServerError.css:
--------------------------------------------------------------------------------
1 | .server-error-page {
2 | max-width: 500px;
3 | margin: 0 auto;
4 | margin-top: 50px;
5 | padding: 40px;
6 | border: 1px solid #c8c8c8;
7 | text-align: center;
8 | }
9 |
10 | .server-error-page .server-error-title {
11 | font-size: 50px;
12 | letter-spacing: 10px;
13 | margin-bottom: 10px;
14 | color: #f44336;
15 | }
16 |
17 | .server-error-page .server-error-desc {
18 | font-size: 20px;
19 | margin-bottom: 20px;
20 | }
21 |
22 | .server-error-go-back-btn {
23 | min-width: 160px;
24 | }
--------------------------------------------------------------------------------
/polling-app-client/src/common/ServerError.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react';
2 | import './ServerError.css';
3 | import { Link } from 'react-router-dom';
4 | import { Button } from 'antd';
5 |
6 | class ServerError extends Component {
7 | render() {
8 | return (
9 |
10 |
11 | 500
12 |
13 |
14 | Oops! Something went wrong at our Server. Why don't you go back?
15 |
16 |
Go Back
17 |
18 | );
19 | }
20 | }
21 |
22 | export default ServerError;
--------------------------------------------------------------------------------
/polling-app-client/src/constants/index.js:
--------------------------------------------------------------------------------
1 | export const API_BASE_URL = process.env.REACT_APP_API_BASE_URL || 'http://localhost:8080/api';
2 | export const ACCESS_TOKEN = 'accessToken';
3 |
4 | export const POLL_LIST_SIZE = 30;
5 | export const MAX_CHOICES = 6;
6 | export const POLL_QUESTION_MAX_LENGTH = 140;
7 | export const POLL_CHOICE_MAX_LENGTH = 40;
8 |
9 | export const NAME_MIN_LENGTH = 4;
10 | export const NAME_MAX_LENGTH = 40;
11 |
12 | export const USERNAME_MIN_LENGTH = 3;
13 | export const USERNAME_MAX_LENGTH = 15;
14 |
15 | export const EMAIL_MAX_LENGTH = 40;
16 |
17 | export const PASSWORD_MIN_LENGTH = 6;
18 | export const PASSWORD_MAX_LENGTH = 20;
19 |
--------------------------------------------------------------------------------
/polling-app-client/src/index.css:
--------------------------------------------------------------------------------
1 | body {
2 | margin: 0;
3 | padding: 0;
4 | font-family: sans-serif;
5 | }
6 |
7 | .container {
8 | max-width: 1000px;
9 | margin-left: auto;
10 | margin-right: auto;
11 | padding-left: 15px;
12 | padding-right: 15px;
13 | }
14 |
15 | @media (max-width: 768px) {
16 | h1 {
17 | font-size: 1.7em;
18 | }
19 | }
--------------------------------------------------------------------------------
/polling-app-client/src/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import ReactDOM from 'react-dom';
3 | import './index.css';
4 | import App from './app/App';
5 | import registerServiceWorker from './registerServiceWorker';
6 | import { BrowserRouter as Router } from 'react-router-dom';
7 |
8 | ReactDOM.render(
9 |
10 |
11 | ,
12 | document.getElementById('root')
13 | );
14 |
15 | registerServiceWorker();
16 |
--------------------------------------------------------------------------------
/polling-app-client/src/logo.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
--------------------------------------------------------------------------------
/polling-app-client/src/poll.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
--------------------------------------------------------------------------------
/polling-app-client/src/poll/NewPoll.css:
--------------------------------------------------------------------------------
1 | .new-poll-container {
2 | max-width: 520px;
3 | margin: 0 auto;
4 | margin-top: 40px;
5 | }
6 |
7 | .create-poll-form-button {
8 | width: 100%;
9 | }
10 |
11 | .create-poll-form-button[disabled], .create-poll-form-button[disabled]:hover, .create-poll-form-button[disabled]:focus {
12 | opacity: 0.6;
13 | color: #fff;
14 | background-color: #1890ff;
15 | border-color: #1890ff;
16 | }
17 |
18 |
19 | .dynamic-delete-button {
20 | cursor: pointer;
21 | position: relative;
22 | top: 4px;
23 | font-size: 24px;
24 | color: #999;
25 | transition: all .3s;
26 | }
27 |
28 | .optional-choice {
29 | width: calc(100% - 35px);
30 | margin-right: 8px;
31 | }
32 |
33 | .dynamic-delete-button:hover {
34 | color: #777;
35 | }
36 |
37 | .dynamic-delete-button[disabled] {
38 | cursor: not-allowed;
39 | opacity: 0.5;
40 | }
41 |
42 | .poll-form-row {
43 | margin-bottom: 20px;
44 | }
45 |
46 | .poll-form-row input, .poll-form-row textarea {
47 | margin-bottom: 4px;
48 | }
--------------------------------------------------------------------------------
/polling-app-client/src/poll/NewPoll.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react';
2 | import { createPoll } from '../util/APIUtils';
3 | import { MAX_CHOICES, POLL_QUESTION_MAX_LENGTH, POLL_CHOICE_MAX_LENGTH } from '../constants';
4 | import './NewPoll.css';
5 | import { Form, Input, Button, Icon, Select, Col, notification } from 'antd';
6 | const Option = Select.Option;
7 | const FormItem = Form.Item;
8 | const { TextArea } = Input
9 |
10 | class NewPoll extends Component {
11 | constructor(props) {
12 | super(props);
13 | this.state = {
14 | question: {
15 | text: ''
16 | },
17 | choices: [{
18 | text: ''
19 | }, {
20 | text: ''
21 | }],
22 | pollLength: {
23 | days: 1,
24 | hours: 0
25 | }
26 | };
27 | this.addChoice = this.addChoice.bind(this);
28 | this.removeChoice = this.removeChoice.bind(this);
29 | this.handleSubmit = this.handleSubmit.bind(this);
30 | this.handleQuestionChange = this.handleQuestionChange.bind(this);
31 | this.handleChoiceChange = this.handleChoiceChange.bind(this);
32 | this.handlePollDaysChange = this.handlePollDaysChange.bind(this);
33 | this.handlePollHoursChange = this.handlePollHoursChange.bind(this);
34 | this.isFormInvalid = this.isFormInvalid.bind(this);
35 | }
36 |
37 | addChoice(event) {
38 | const choices = this.state.choices.slice();
39 | this.setState({
40 | choices: choices.concat([{
41 | text: ''
42 | }])
43 | });
44 | }
45 |
46 | removeChoice(choiceNumber) {
47 | const choices = this.state.choices.slice();
48 | this.setState({
49 | choices: [...choices.slice(0, choiceNumber), ...choices.slice(choiceNumber+1)]
50 | });
51 | }
52 |
53 | handleSubmit(event) {
54 | event.preventDefault();
55 | const pollData = {
56 | question: this.state.question.text,
57 | choices: this.state.choices.map(choice => {
58 | return {text: choice.text}
59 | }),
60 | pollLength: this.state.pollLength
61 | };
62 |
63 | createPoll(pollData)
64 | .then(response => {
65 | this.props.history.push("/");
66 | }).catch(error => {
67 | if(error.status === 401) {
68 | this.props.handleLogout('/login', 'error', 'You have been logged out. Please login create poll.');
69 | } else {
70 | notification.error({
71 | message: 'Polling App',
72 | description: error.message || 'Sorry! Something went wrong. Please try again!'
73 | });
74 | }
75 | });
76 | }
77 |
78 | validateQuestion = (questionText) => {
79 | if(questionText.length === 0) {
80 | return {
81 | validateStatus: 'error',
82 | errorMsg: 'Please enter your question!'
83 | }
84 | } else if (questionText.length > POLL_QUESTION_MAX_LENGTH) {
85 | return {
86 | validateStatus: 'error',
87 | errorMsg: `Question is too long (Maximum ${POLL_QUESTION_MAX_LENGTH} characters allowed)`
88 | }
89 | } else {
90 | return {
91 | validateStatus: 'success',
92 | errorMsg: null
93 | }
94 | }
95 | }
96 |
97 | handleQuestionChange(event) {
98 | const value = event.target.value;
99 | this.setState({
100 | question: {
101 | text: value,
102 | ...this.validateQuestion(value)
103 | }
104 | });
105 | }
106 |
107 | validateChoice = (choiceText) => {
108 | if(choiceText.length === 0) {
109 | return {
110 | validateStatus: 'error',
111 | errorMsg: 'Please enter a choice!'
112 | }
113 | } else if (choiceText.length > POLL_CHOICE_MAX_LENGTH) {
114 | return {
115 | validateStatus: 'error',
116 | errorMsg: `Choice is too long (Maximum ${POLL_CHOICE_MAX_LENGTH} characters allowed)`
117 | }
118 | } else {
119 | return {
120 | validateStatus: 'success',
121 | errorMsg: null
122 | }
123 | }
124 | }
125 |
126 | handleChoiceChange(event, index) {
127 | const choices = this.state.choices.slice();
128 | const value = event.target.value;
129 |
130 | choices[index] = {
131 | text: value,
132 | ...this.validateChoice(value)
133 | }
134 |
135 | this.setState({
136 | choices: choices
137 | });
138 | }
139 |
140 |
141 | handlePollDaysChange(value) {
142 | const pollLength = Object.assign(this.state.pollLength, {days: value});
143 | this.setState({
144 | pollLength: pollLength
145 | });
146 | }
147 |
148 | handlePollHoursChange(value) {
149 | const pollLength = Object.assign(this.state.pollLength, {hours: value});
150 | this.setState({
151 | pollLength: pollLength
152 | });
153 | }
154 |
155 | isFormInvalid() {
156 | if(this.state.question.validateStatus !== 'success') {
157 | return true;
158 | }
159 |
160 | for(let i = 0; i < this.state.choices.length; i++) {
161 | const choice = this.state.choices[i];
162 | if(choice.validateStatus !== 'success') {
163 | return true;
164 | }
165 | }
166 | }
167 |
168 | render() {
169 | const choiceViews = [];
170 | this.state.choices.forEach((choice, index) => {
171 | choiceViews.push();
172 | });
173 |
174 | return (
175 |
176 |
Create Poll
177 |
178 |
238 |
239 |
240 | );
241 | }
242 | }
243 |
244 | function PollChoice(props) {
245 | return (
246 |
248 | 1 ? "optional-choice": null}
253 | onChange={(event) => props.handleChoiceChange(event, props.choiceNumber)} />
254 |
255 | {
256 | props.choiceNumber > 1 ? (
257 | props.removeChoice(props.choiceNumber)}
262 | /> ): null
263 | }
264 |
265 | );
266 | }
267 |
268 |
269 | export default NewPoll;
--------------------------------------------------------------------------------
/polling-app-client/src/poll/Poll.css:
--------------------------------------------------------------------------------
1 | .polls-container {
2 | max-width: 600px;
3 | margin: 0 auto;
4 | margin-top: 20px;
5 | }
6 |
7 | .poll-content {
8 | margin-bottom: 30px;
9 | padding: 20px 15px 20px 15px;
10 | letter-spacing: .01em;
11 | box-shadow: 0 2px 2px 0 rgba(0,0,0,0.16), 0 0 0 1px rgba(0,0,0,0.08);
12 | }
13 |
14 | .poll-question {
15 | font-weight: 500;
16 | font-size: 21px;
17 | color: rgba(0, 0, 0, 0.85);
18 | margin-bottom: 10px;
19 | }
20 |
21 | @media (max-width: 768px) {
22 | .poll-question {
23 | font-size: 18px;
24 | }
25 | }
26 |
27 | .poll-choices {
28 | margin-top: 5px;
29 | margin-bottom: 15px;
30 | }
31 |
32 | .poll-choice-radio-group {
33 | display: block;
34 | }
35 |
36 | .poll-choice-radio {
37 | display: block;
38 | line-height: 1.7;
39 | font-size: 15px;
40 | padding-top: 6px;
41 | padding-bottom: 6px;
42 | max-width: 100%;
43 | white-space: nowrap;
44 | text-overflow: ellipsis;
45 | overflow: hidden;
46 | color: rgba(0, 0, 0, 0.75);
47 | }
48 |
49 | .vote-button {
50 | border-radius: 15px;
51 | margin-right: 15px;
52 | color: #40a9ff;
53 | border-color: #40a9ff;
54 | }
55 |
56 | .vote-button:hover {
57 | color: #1890ff;
58 | border-color: #1890ff;
59 | }
60 |
61 | .vote-button[disabled], .vote-button[disabled]:hover, .vote-button[disabled]:focus {
62 | color: #40a9ff;
63 | border-color: #40a9ff;
64 | background-color: #fff;
65 | opacity: 0.4;
66 | }
67 |
68 | .separator {
69 | margin-left: 10px;
70 | margin-right: 10px;
71 | color: #8899A6;
72 | }
73 |
74 | .cv-poll-choice {
75 | position: relative;
76 | margin-bottom: 8px;
77 | }
78 |
79 | .cv-poll-choice-details {
80 | position: relative;
81 | z-index: 1;
82 | display: block;
83 | line-height: 1.7;
84 | font-size: 15px;
85 | padding-left: 10px;
86 | padding-top: 6px;
87 | padding-bottom: 6px;
88 | max-width: 100%;
89 | white-space: nowrap;
90 | text-overflow: ellipsis;
91 | overflow: hidden;
92 | }
93 |
94 | .cv-choice-percentage {
95 | font-weight: 600;
96 | color: rgba(0, 0, 0, 0.75);
97 | }
98 |
99 | .cv-choice-text {
100 | margin-left: 10px;
101 | display: inline-block;
102 | vertical-align: bottom;
103 | text-overflow: ellipsis;
104 | white-space: nowrap;
105 | overflow: hidden;
106 | }
107 |
108 | .selected-choice-icon {
109 | margin-left: 10px;
110 | font-weight: 600;
111 | color: rgba(0, 0, 0, 0.75);
112 | }
113 |
114 | .cv-choice-percent-chart {
115 | position: absolute;
116 | background: #E1E8ED;
117 | top: 0;
118 | left: 0;
119 | height: 100%;
120 | border-radius: .35em;
121 | transition: all .3s cubic-bezier(0.5,1.2,.5,1.2);
122 | opacity: 1;
123 | }
124 |
125 | .cv-choice-percent-chart.winner {
126 | background-color: #77C7F7;
127 | }
128 |
129 | .poll-creator-info {
130 | margin-left: 58px;
131 | margin-bottom: 10px;
132 | height: 58px;
133 | }
134 |
135 | .poll-creator-info .creator-link {
136 | display: block;
137 | text-overflow: ellipsis;
138 | overflow: hidden;
139 | white-space: nowrap;
140 | }
141 |
142 | .poll-creator-info .creator-link:hover .poll-creator-name {
143 | color: #1890ff;
144 | text-decoration: underline;
145 | }
146 |
147 | .poll-creator-avatar {
148 | float: left;
149 | margin-top: 3px;
150 | margin-left: -58px;
151 | position: absolute;
152 | width: 48px;
153 | height: 48px;
154 | line-height: 48px;
155 | border-radius: 24px;
156 | }
157 |
158 | .poll-creator-avatar > * {
159 | line-height: 48px;
160 | font-size: 18px;
161 | }
162 |
163 | .poll-creator-name {
164 | font-size: 16px;
165 | font-weight: 500;
166 | color: rgba(0, 0, 0, 0.85);
167 | margin-top: 4px;
168 | display: inline-block;
169 | margin-right: 8px;
170 | }
171 |
172 | .poll-creator-username {
173 | color: #657786;
174 | font-size: 15px;
175 | display: inline-block;
176 | }
177 |
178 | .poll-creation-date {
179 | display: block;
180 | color: #657786;
181 | font-size: 13px;
182 | margin-top: 2px;
183 | }
--------------------------------------------------------------------------------
/polling-app-client/src/poll/Poll.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react';
2 | import './Poll.css';
3 | import { Avatar, Icon } from 'antd';
4 | import { Link } from 'react-router-dom';
5 | import { getAvatarColor } from '../util/Colors';
6 | import { formatDateTime } from '../util/Helpers';
7 |
8 | import { Radio, Button } from 'antd';
9 | const RadioGroup = Radio.Group;
10 |
11 | class Poll extends Component {
12 | calculatePercentage = (choice) => {
13 | if(this.props.poll.totalVotes === 0) {
14 | return 0;
15 | }
16 | return (choice.voteCount*100)/(this.props.poll.totalVotes);
17 | };
18 |
19 | isSelected = (choice) => {
20 | return this.props.poll.selectedChoice === choice.id;
21 | }
22 |
23 | getWinningChoice = () => {
24 | return this.props.poll.choices.reduce((prevChoice, currentChoice) =>
25 | currentChoice.voteCount > prevChoice.voteCount ? currentChoice : prevChoice,
26 | {voteCount: -Infinity}
27 | );
28 | }
29 |
30 | getTimeRemaining = (poll) => {
31 | const expirationTime = new Date(poll.expirationDateTime).getTime();
32 | const currentTime = new Date().getTime();
33 |
34 | var difference_ms = expirationTime - currentTime;
35 | var seconds = Math.floor( (difference_ms/1000) % 60 );
36 | var minutes = Math.floor( (difference_ms/1000/60) % 60 );
37 | var hours = Math.floor( (difference_ms/(1000*60*60)) % 24 );
38 | var days = Math.floor( difference_ms/(1000*60*60*24) );
39 |
40 | let timeRemaining;
41 |
42 | if(days > 0) {
43 | timeRemaining = days + " days left";
44 | } else if (hours > 0) {
45 | timeRemaining = hours + " hours left";
46 | } else if (minutes > 0) {
47 | timeRemaining = minutes + " minutes left";
48 | } else if(seconds > 0) {
49 | timeRemaining = seconds + " seconds left";
50 | } else {
51 | timeRemaining = "less than a second left";
52 | }
53 |
54 | return timeRemaining;
55 | }
56 |
57 | render() {
58 | const pollChoices = [];
59 | if(this.props.poll.selectedChoice || this.props.poll.expired) {
60 | const winningChoice = this.props.poll.expired ? this.getWinningChoice() : null;
61 |
62 | this.props.poll.choices.forEach(choice => {
63 | pollChoices.push( );
70 | });
71 | } else {
72 | this.props.poll.choices.forEach(choice => {
73 | pollChoices.push({choice.text} )
74 | })
75 | }
76 | return (
77 |
78 |
79 |
80 |
81 |
83 | {this.props.poll.createdBy.name[0].toUpperCase()}
84 |
85 |
86 | {this.props.poll.createdBy.name}
87 |
88 |
89 | @{this.props.poll.createdBy.username}
90 |
91 |
92 | {formatDateTime(this.props.poll.creationDateTime)}
93 |
94 |
95 |
96 |
97 | {this.props.poll.question}
98 |
99 |
100 |
101 |
105 | { pollChoices }
106 |
107 |
108 |
109 | {
110 | !(this.props.poll.selectedChoice || this.props.poll.expired) ?
111 | (Vote ) : null
112 | }
113 | {this.props.poll.totalVotes} votes
114 | •
115 |
116 | {
117 | this.props.poll.expired ? "Final results" :
118 | this.getTimeRemaining(this.props.poll)
119 | }
120 |
121 |
122 |
123 | );
124 | }
125 | }
126 |
127 | function CompletedOrVotedPollChoice(props) {
128 | return (
129 |
130 |
131 |
132 | {Math.round(props.percentVote * 100) / 100}%
133 |
134 |
135 | {props.choice.text}
136 |
137 | {
138 | props.isSelected ? (
139 | ): null
143 | }
144 |
145 |
147 |
148 |
149 | );
150 | }
151 |
152 |
153 | export default Poll;
--------------------------------------------------------------------------------
/polling-app-client/src/poll/PollList.css:
--------------------------------------------------------------------------------
1 | .no-polls-found {
2 | font-size: 20px;
3 | text-align: center;
4 | padding: 20px;
5 | }
6 |
7 | .load-more-polls {
8 | text-align: center;
9 | margin-bottom: 40px;
10 | }
--------------------------------------------------------------------------------
/polling-app-client/src/poll/PollList.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react';
2 | import { getAllPolls, getUserCreatedPolls, getUserVotedPolls } from '../util/APIUtils';
3 | import Poll from './Poll';
4 | import { castVote } from '../util/APIUtils';
5 | import LoadingIndicator from '../common/LoadingIndicator';
6 | import { Button, Icon, notification } from 'antd';
7 | import { POLL_LIST_SIZE } from '../constants';
8 | import { withRouter } from 'react-router-dom';
9 | import './PollList.css';
10 |
11 | class PollList extends Component {
12 | constructor(props) {
13 | super(props);
14 | this.state = {
15 | polls: [],
16 | page: 0,
17 | size: 10,
18 | totalElements: 0,
19 | totalPages: 0,
20 | last: true,
21 | currentVotes: [],
22 | isLoading: false
23 | };
24 | this.loadPollList = this.loadPollList.bind(this);
25 | this.handleLoadMore = this.handleLoadMore.bind(this);
26 | }
27 |
28 | loadPollList(page = 0, size = POLL_LIST_SIZE) {
29 | let promise;
30 | if(this.props.username) {
31 | if(this.props.type === 'USER_CREATED_POLLS') {
32 | promise = getUserCreatedPolls(this.props.username, page, size);
33 | } else if (this.props.type === 'USER_VOTED_POLLS') {
34 | promise = getUserVotedPolls(this.props.username, page, size);
35 | }
36 | } else {
37 | promise = getAllPolls(page, size);
38 | }
39 |
40 | if(!promise) {
41 | return;
42 | }
43 |
44 | this.setState({
45 | isLoading: true
46 | });
47 |
48 | promise
49 | .then(response => {
50 | const polls = this.state.polls.slice();
51 | const currentVotes = this.state.currentVotes.slice();
52 |
53 | this.setState({
54 | polls: polls.concat(response.content),
55 | page: response.page,
56 | size: response.size,
57 | totalElements: response.totalElements,
58 | totalPages: response.totalPages,
59 | last: response.last,
60 | currentVotes: currentVotes.concat(Array(response.content.length).fill(null)),
61 | isLoading: false
62 | })
63 | }).catch(error => {
64 | this.setState({
65 | isLoading: false
66 | })
67 | });
68 |
69 | }
70 |
71 | componentDidMount() {
72 | this.loadPollList();
73 | }
74 |
75 | componentDidUpdate(nextProps) {
76 | if(this.props.isAuthenticated !== nextProps.isAuthenticated) {
77 | // Reset State
78 | this.setState({
79 | polls: [],
80 | page: 0,
81 | size: 10,
82 | totalElements: 0,
83 | totalPages: 0,
84 | last: true,
85 | currentVotes: [],
86 | isLoading: false
87 | });
88 | this.loadPollList();
89 | }
90 | }
91 |
92 | handleLoadMore() {
93 | this.loadPollList(this.state.page + 1);
94 | }
95 |
96 | handleVoteChange(event, pollIndex) {
97 | const currentVotes = this.state.currentVotes.slice();
98 | currentVotes[pollIndex] = event.target.value;
99 |
100 | this.setState({
101 | currentVotes: currentVotes
102 | });
103 | }
104 |
105 |
106 | handleVoteSubmit(event, pollIndex) {
107 | event.preventDefault();
108 | if(!this.props.isAuthenticated) {
109 | this.props.history.push("/login");
110 | notification.info({
111 | message: 'Polling App',
112 | description: "Please login to vote.",
113 | });
114 | return;
115 | }
116 |
117 | const poll = this.state.polls[pollIndex];
118 | const selectedChoice = this.state.currentVotes[pollIndex];
119 |
120 | const voteData = {
121 | pollId: poll.id,
122 | choiceId: selectedChoice
123 | };
124 |
125 | castVote(voteData)
126 | .then(response => {
127 | const polls = this.state.polls.slice();
128 | polls[pollIndex] = response;
129 | this.setState({
130 | polls: polls
131 | });
132 | }).catch(error => {
133 | if(error.status === 401) {
134 | this.props.handleLogout('/login', 'error', 'You have been logged out. Please login to vote');
135 | } else {
136 | notification.error({
137 | message: 'Polling App',
138 | description: error.message || 'Sorry! Something went wrong. Please try again!'
139 | });
140 | }
141 | });
142 | }
143 |
144 | render() {
145 | const pollViews = [];
146 | this.state.polls.forEach((poll, pollIndex) => {
147 | pollViews.push( this.handleVoteChange(event, pollIndex)}
152 | handleVoteSubmit={(event) => this.handleVoteSubmit(event, pollIndex)} />)
153 | });
154 |
155 | return (
156 |
157 | {pollViews}
158 | {
159 | !this.state.isLoading && this.state.polls.length === 0 ? (
160 |
161 | No Polls Found.
162 |
163 | ): null
164 | }
165 | {
166 | !this.state.isLoading && !this.state.last ? (
167 |
168 |
169 | Load more
170 |
171 |
): null
172 | }
173 | {
174 | this.state.isLoading ?
175 |
: null
176 | }
177 |
178 | );
179 | }
180 | }
181 |
182 | export default withRouter(PollList);
--------------------------------------------------------------------------------
/polling-app-client/src/registerServiceWorker.js:
--------------------------------------------------------------------------------
1 | // In production, we register a service worker to serve assets from local cache.
2 |
3 | // This lets the app load faster on subsequent visits in production, and gives
4 | // it offline capabilities. However, it also means that developers (and users)
5 | // will only see deployed updates on the "N+1" visit to a page, since previously
6 | // cached resources are updated in the background.
7 |
8 | // To learn more about the benefits of this model, read https://goo.gl/KwvDNy.
9 | // This link also includes instructions on opting out of this behavior.
10 |
11 | const isLocalhost = Boolean(
12 | window.location.hostname === 'localhost' ||
13 | // [::1] is the IPv6 localhost address.
14 | window.location.hostname === '[::1]' ||
15 | // 127.0.0.1/8 is considered localhost for IPv4.
16 | window.location.hostname.match(
17 | /^127(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/
18 | )
19 | );
20 |
21 | export default function register() {
22 | if (process.env.NODE_ENV === 'production' && 'serviceWorker' in navigator) {
23 | // The URL constructor is available in all browsers that support SW.
24 | const publicUrl = new URL(process.env.PUBLIC_URL, window.location);
25 | if (publicUrl.origin !== window.location.origin) {
26 | // Our service worker won't work if PUBLIC_URL is on a different origin
27 | // from what our page is served on. This might happen if a CDN is used to
28 | // serve assets; see https://github.com/facebookincubator/create-react-app/issues/2374
29 | return;
30 | }
31 |
32 | window.addEventListener('load', () => {
33 | const swUrl = `${process.env.PUBLIC_URL}/service-worker.js`;
34 |
35 | if (isLocalhost) {
36 | // This is running on localhost. Lets check if a service worker still exists or not.
37 | checkValidServiceWorker(swUrl);
38 |
39 | // Add some additional logging to localhost, pointing developers to the
40 | // service worker/PWA documentation.
41 | navigator.serviceWorker.ready.then(() => {
42 | console.log(
43 | 'This web app is being served cache-first by a service ' +
44 | 'worker. To learn more, visit https://goo.gl/SC7cgQ'
45 | );
46 | });
47 | } else {
48 | // Is not local host. Just register service worker
49 | registerValidSW(swUrl);
50 | }
51 | });
52 | }
53 | }
54 |
55 | function registerValidSW(swUrl) {
56 | navigator.serviceWorker
57 | .register(swUrl)
58 | .then(registration => {
59 | registration.onupdatefound = () => {
60 | const installingWorker = registration.installing;
61 | installingWorker.onstatechange = () => {
62 | if (installingWorker.state === 'installed') {
63 | if (navigator.serviceWorker.controller) {
64 | // At this point, the old content will have been purged and
65 | // the fresh content will have been added to the cache.
66 | // It's the perfect time to display a "New content is
67 | // available; please refresh." message in your web app.
68 | console.log('New content is available; please refresh.');
69 | } else {
70 | // At this point, everything has been precached.
71 | // It's the perfect time to display a
72 | // "Content is cached for offline use." message.
73 | console.log('Content is cached for offline use.');
74 | }
75 | }
76 | };
77 | };
78 | })
79 | .catch(error => {
80 | console.error('Error during service worker registration:', error);
81 | });
82 | }
83 |
84 | function checkValidServiceWorker(swUrl) {
85 | // Check if the service worker can be found. If it can't reload the page.
86 | fetch(swUrl)
87 | .then(response => {
88 | // Ensure service worker exists, and that we really are getting a JS file.
89 | if (
90 | response.status === 404 ||
91 | response.headers.get('content-type').indexOf('javascript') === -1
92 | ) {
93 | // No service worker found. Probably a different app. Reload the page.
94 | navigator.serviceWorker.ready.then(registration => {
95 | registration.unregister().then(() => {
96 | window.location.reload();
97 | });
98 | });
99 | } else {
100 | // Service worker found. Proceed as normal.
101 | registerValidSW(swUrl);
102 | }
103 | })
104 | .catch(() => {
105 | console.log(
106 | 'No internet connection found. App is running in offline mode.'
107 | );
108 | });
109 | }
110 |
111 | export function unregister() {
112 | if ('serviceWorker' in navigator) {
113 | navigator.serviceWorker.ready.then(registration => {
114 | registration.unregister();
115 | });
116 | }
117 | }
118 |
--------------------------------------------------------------------------------
/polling-app-client/src/user/login/Login.css:
--------------------------------------------------------------------------------
1 | .login-container {
2 | max-width: 420px;
3 | margin: 0 auto;
4 | margin: 0 auto;
5 | margin-top: 40px;
6 | }
7 |
8 | .login-form-button {
9 | width: 100%;
10 | }
--------------------------------------------------------------------------------
/polling-app-client/src/user/login/Login.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react';
2 | import { login } from '../../util/APIUtils';
3 | import './Login.css';
4 | import { Link } from 'react-router-dom';
5 | import { ACCESS_TOKEN } from '../../constants';
6 |
7 | import { Form, Input, Button, Icon, notification } from 'antd';
8 | const FormItem = Form.Item;
9 |
10 | class Login extends Component {
11 | render() {
12 | const AntWrappedLoginForm = Form.create()(LoginForm)
13 | return (
14 |
20 | );
21 | }
22 | }
23 |
24 | class LoginForm extends Component {
25 | constructor(props) {
26 | super(props);
27 | this.handleSubmit = this.handleSubmit.bind(this);
28 | }
29 |
30 | handleSubmit(event) {
31 | event.preventDefault();
32 | this.props.form.validateFields((err, values) => {
33 | if (!err) {
34 | const loginRequest = Object.assign({}, values);
35 | login(loginRequest)
36 | .then(response => {
37 | localStorage.setItem(ACCESS_TOKEN, response.accessToken);
38 | this.props.onLogin();
39 | }).catch(error => {
40 | if(error.status === 401) {
41 | notification.error({
42 | message: 'Polling App',
43 | description: 'Your Username or Password is incorrect. Please try again!'
44 | });
45 | } else {
46 | notification.error({
47 | message: 'Polling App',
48 | description: error.message || 'Sorry! Something went wrong. Please try again!'
49 | });
50 | }
51 | });
52 | }
53 | });
54 | }
55 |
56 | render() {
57 | const { getFieldDecorator } = this.props.form;
58 | return (
59 |
60 |
61 | {getFieldDecorator('usernameOrEmail', {
62 | rules: [{ required: true, message: 'Please input your username or email!' }],
63 | })(
64 | }
66 | size="large"
67 | name="usernameOrEmail"
68 | placeholder="Username or Email" />
69 | )}
70 |
71 |
72 | {getFieldDecorator('password', {
73 | rules: [{ required: true, message: 'Please input your Password!' }],
74 | })(
75 | }
77 | size="large"
78 | name="password"
79 | type="password"
80 | placeholder="Password" />
81 | )}
82 |
83 |
84 | Login
85 | Or register now!
86 |
87 |
88 | );
89 | }
90 | }
91 |
92 |
93 | export default Login;
--------------------------------------------------------------------------------
/polling-app-client/src/user/profile/Profile.css:
--------------------------------------------------------------------------------
1 | .user-details {
2 | margin-bottom: 44px;
3 | padding-top: 40px;
4 | padding-bottom: 20px;
5 | margin: 0 auto;
6 | text-align: center;
7 | }
8 |
9 | .user-avatar-circle {
10 | width: 120px;
11 | height: 120px;
12 | border-radius: 60px;
13 | line-height: 60px;
14 | }
15 |
16 | .user-avatar-circle > * {
17 | line-height: 120px;
18 | font-size: 40px;
19 | position: relative !important;
20 | left: 0 !important;
21 | }
22 |
23 | .user-summary {
24 | text-align: center;
25 | padding-top: 20px;
26 | }
27 |
28 | @media (min-width: 576px) {
29 | .user-details {
30 | text-align: left;
31 | display: table;
32 | }
33 |
34 | .user-avatar, .user-summary {
35 | float: left;
36 | text-align: left;
37 | }
38 |
39 | .user-summary {
40 | padding-top: 0;
41 | padding-left: 40px;
42 | width: calc(100% - 120px);
43 | }
44 | }
45 |
46 | .user-summary .full-name {
47 | font-size: 30px;
48 | font-weight: 600;
49 | color: rgba(0,0,0,0.85);
50 | text-overflow: ellipsis;
51 | overflow: hidden;
52 | white-space: nowrap;
53 | }
54 |
55 | .user-summary .username {
56 | font-size: 20px;
57 | }
58 |
59 | .user-summary .user-joined {
60 | margin-top: 5px;
61 | }
62 |
63 | @media (max-width: 768px) {
64 | .user-summary .full-name {
65 | font-size: 24px;
66 | }
67 | }
68 |
69 |
--------------------------------------------------------------------------------
/polling-app-client/src/user/profile/Profile.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react';
2 | import PollList from '../../poll/PollList';
3 | import { getUserProfile } from '../../util/APIUtils';
4 | import { Avatar, Tabs } from 'antd';
5 | import { getAvatarColor } from '../../util/Colors';
6 | import { formatDate } from '../../util/Helpers';
7 | import LoadingIndicator from '../../common/LoadingIndicator';
8 | import './Profile.css';
9 | import NotFound from '../../common/NotFound';
10 | import ServerError from '../../common/ServerError';
11 |
12 | const TabPane = Tabs.TabPane;
13 |
14 | class Profile extends Component {
15 | constructor(props) {
16 | super(props);
17 | this.state = {
18 | user: null,
19 | isLoading: false
20 | }
21 | this.loadUserProfile = this.loadUserProfile.bind(this);
22 | }
23 |
24 | loadUserProfile(username) {
25 | this.setState({
26 | isLoading: true
27 | });
28 |
29 | getUserProfile(username)
30 | .then(response => {
31 | this.setState({
32 | user: response,
33 | isLoading: false
34 | });
35 | }).catch(error => {
36 | if(error.status === 404) {
37 | this.setState({
38 | notFound: true,
39 | isLoading: false
40 | });
41 | } else {
42 | this.setState({
43 | serverError: true,
44 | isLoading: false
45 | });
46 | }
47 | });
48 | }
49 |
50 | componentDidMount() {
51 | const username = this.props.match.params.username;
52 | this.loadUserProfile(username);
53 | }
54 |
55 | componentDidUpdate(nextProps) {
56 | if(this.props.match.params.username !== nextProps.match.params.username) {
57 | this.loadUserProfile(nextProps.match.params.username);
58 | }
59 | }
60 |
61 | render() {
62 | if(this.state.isLoading) {
63 | return ;
64 | }
65 |
66 | if(this.state.notFound) {
67 | return ;
68 | }
69 |
70 | if(this.state.serverError) {
71 | return ;
72 | }
73 |
74 | const tabBarStyle = {
75 | textAlign: 'center'
76 | };
77 |
78 | return (
79 |
80 | {
81 | this.state.user ? (
82 |
83 |
84 |
85 |
86 | {this.state.user.name[0].toUpperCase()}
87 |
88 |
89 |
90 |
{this.state.user.name}
91 |
@{this.state.user.username}
92 |
93 | Joined {formatDate(this.state.user.joinedAt)}
94 |
95 |
96 |
97 |
98 |
103 |
104 |
105 |
106 |
107 |
108 |
109 |
110 |
111 |
112 | ): null
113 | }
114 |
115 | );
116 | }
117 | }
118 |
119 | export default Profile;
--------------------------------------------------------------------------------
/polling-app-client/src/user/signup/Signup.css:
--------------------------------------------------------------------------------
1 | .signup-container {
2 | max-width: 420px;
3 | margin: 0 auto;
4 | margin-top: 40px;
5 | }
6 |
7 | .signup-form-button {
8 | width: 100%;
9 | margin-top: 15px;
10 | }
11 |
12 | .signup-form-button[disabled], .signup-form-button[disabled]:hover, .signup-form-button[disabled]:focus {
13 | opacity: 0.6;
14 | color: #fff;
15 | background-color: #1890ff;
16 | border-color: #1890ff;
17 | }
18 |
19 | .signup-form input {
20 | margin-bottom: 3px;
21 | }
--------------------------------------------------------------------------------
/polling-app-client/src/user/signup/Signup.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react';
2 | import { signup, checkUsernameAvailability, checkEmailAvailability } from '../../util/APIUtils';
3 | import './Signup.css';
4 | import { Link } from 'react-router-dom';
5 | import {
6 | NAME_MIN_LENGTH, NAME_MAX_LENGTH,
7 | USERNAME_MIN_LENGTH, USERNAME_MAX_LENGTH,
8 | EMAIL_MAX_LENGTH,
9 | PASSWORD_MIN_LENGTH, PASSWORD_MAX_LENGTH
10 | } from '../../constants';
11 |
12 | import { Form, Input, Button, notification } from 'antd';
13 | const FormItem = Form.Item;
14 |
15 | class Signup extends Component {
16 | constructor(props) {
17 | super(props);
18 | this.state = {
19 | name: {
20 | value: ''
21 | },
22 | username: {
23 | value: ''
24 | },
25 | email: {
26 | value: ''
27 | },
28 | password: {
29 | value: ''
30 | }
31 | }
32 | this.handleInputChange = this.handleInputChange.bind(this);
33 | this.handleSubmit = this.handleSubmit.bind(this);
34 | this.validateUsernameAvailability = this.validateUsernameAvailability.bind(this);
35 | this.validateEmailAvailability = this.validateEmailAvailability.bind(this);
36 | this.isFormInvalid = this.isFormInvalid.bind(this);
37 | }
38 |
39 | handleInputChange(event, validationFun) {
40 | const target = event.target;
41 | const inputName = target.name;
42 | const inputValue = target.value;
43 |
44 | this.setState({
45 | [inputName] : {
46 | value: inputValue,
47 | ...validationFun(inputValue)
48 | }
49 | });
50 | }
51 |
52 | handleSubmit(event) {
53 | event.preventDefault();
54 |
55 | const signupRequest = {
56 | name: this.state.name.value,
57 | email: this.state.email.value,
58 | username: this.state.username.value,
59 | password: this.state.password.value
60 | };
61 | signup(signupRequest)
62 | .then(response => {
63 | notification.success({
64 | message: 'Polling App',
65 | description: "Thank you! You're successfully registered. Please Login to continue!",
66 | });
67 | this.props.history.push("/login");
68 | }).catch(error => {
69 | notification.error({
70 | message: 'Polling App',
71 | description: error.message || 'Sorry! Something went wrong. Please try again!'
72 | });
73 | });
74 | }
75 |
76 | isFormInvalid() {
77 | return !(this.state.name.validateStatus === 'success' &&
78 | this.state.username.validateStatus === 'success' &&
79 | this.state.email.validateStatus === 'success' &&
80 | this.state.password.validateStatus === 'success'
81 | );
82 | }
83 |
84 | render() {
85 | return (
86 |
154 | );
155 | }
156 |
157 | // Validation Functions
158 |
159 | validateName = (name) => {
160 | if(name.length < NAME_MIN_LENGTH) {
161 | return {
162 | validateStatus: 'error',
163 | errorMsg: `Name is too short (Minimum ${NAME_MIN_LENGTH} characters needed.)`
164 | }
165 | } else if (name.length > NAME_MAX_LENGTH) {
166 | return {
167 | validationStatus: 'error',
168 | errorMsg: `Name is too long (Maximum ${NAME_MAX_LENGTH} characters allowed.)`
169 | }
170 | } else {
171 | return {
172 | validateStatus: 'success',
173 | errorMsg: null,
174 | };
175 | }
176 | }
177 |
178 | validateEmail = (email) => {
179 | if(!email) {
180 | return {
181 | validateStatus: 'error',
182 | errorMsg: 'Email may not be empty'
183 | }
184 | }
185 |
186 | const EMAIL_REGEX = RegExp('[^@ ]+@[^@ ]+\\.[^@ ]+');
187 | if(!EMAIL_REGEX.test(email)) {
188 | return {
189 | validateStatus: 'error',
190 | errorMsg: 'Email not valid'
191 | }
192 | }
193 |
194 | if(email.length > EMAIL_MAX_LENGTH) {
195 | return {
196 | validateStatus: 'error',
197 | errorMsg: `Email is too long (Maximum ${EMAIL_MAX_LENGTH} characters allowed)`
198 | }
199 | }
200 |
201 | return {
202 | validateStatus: null,
203 | errorMsg: null
204 | }
205 | }
206 |
207 | validateUsername = (username) => {
208 | if(username.length < USERNAME_MIN_LENGTH) {
209 | return {
210 | validateStatus: 'error',
211 | errorMsg: `Username is too short (Minimum ${USERNAME_MIN_LENGTH} characters needed.)`
212 | }
213 | } else if (username.length > USERNAME_MAX_LENGTH) {
214 | return {
215 | validationStatus: 'error',
216 | errorMsg: `Username is too long (Maximum ${USERNAME_MAX_LENGTH} characters allowed.)`
217 | }
218 | } else {
219 | return {
220 | validateStatus: null,
221 | errorMsg: null
222 | }
223 | }
224 | }
225 |
226 | validateUsernameAvailability() {
227 | // First check for client side errors in username
228 | const usernameValue = this.state.username.value;
229 | const usernameValidation = this.validateUsername(usernameValue);
230 |
231 | if(usernameValidation.validateStatus === 'error') {
232 | this.setState({
233 | username: {
234 | value: usernameValue,
235 | ...usernameValidation
236 | }
237 | });
238 | return;
239 | }
240 |
241 | this.setState({
242 | username: {
243 | value: usernameValue,
244 | validateStatus: 'validating',
245 | errorMsg: null
246 | }
247 | });
248 |
249 | checkUsernameAvailability(usernameValue)
250 | .then(response => {
251 | if(response.available) {
252 | this.setState({
253 | username: {
254 | value: usernameValue,
255 | validateStatus: 'success',
256 | errorMsg: null
257 | }
258 | });
259 | } else {
260 | this.setState({
261 | username: {
262 | value: usernameValue,
263 | validateStatus: 'error',
264 | errorMsg: 'This username is already taken'
265 | }
266 | });
267 | }
268 | }).catch(error => {
269 | // Marking validateStatus as success, Form will be recchecked at server
270 | this.setState({
271 | username: {
272 | value: usernameValue,
273 | validateStatus: 'success',
274 | errorMsg: null
275 | }
276 | });
277 | });
278 | }
279 |
280 | validateEmailAvailability() {
281 | // First check for client side errors in email
282 | const emailValue = this.state.email.value;
283 | const emailValidation = this.validateEmail(emailValue);
284 |
285 | if(emailValidation.validateStatus === 'error') {
286 | this.setState({
287 | email: {
288 | value: emailValue,
289 | ...emailValidation
290 | }
291 | });
292 | return;
293 | }
294 |
295 | this.setState({
296 | email: {
297 | value: emailValue,
298 | validateStatus: 'validating',
299 | errorMsg: null
300 | }
301 | });
302 |
303 | checkEmailAvailability(emailValue)
304 | .then(response => {
305 | if(response.available) {
306 | this.setState({
307 | email: {
308 | value: emailValue,
309 | validateStatus: 'success',
310 | errorMsg: null
311 | }
312 | });
313 | } else {
314 | this.setState({
315 | email: {
316 | value: emailValue,
317 | validateStatus: 'error',
318 | errorMsg: 'This Email is already registered'
319 | }
320 | });
321 | }
322 | }).catch(error => {
323 | // Marking validateStatus as success, Form will be recchecked at server
324 | this.setState({
325 | email: {
326 | value: emailValue,
327 | validateStatus: 'success',
328 | errorMsg: null
329 | }
330 | });
331 | });
332 | }
333 |
334 | validatePassword = (password) => {
335 | if(password.length < PASSWORD_MIN_LENGTH) {
336 | return {
337 | validateStatus: 'error',
338 | errorMsg: `Password is too short (Minimum ${PASSWORD_MIN_LENGTH} characters needed.)`
339 | }
340 | } else if (password.length > PASSWORD_MAX_LENGTH) {
341 | return {
342 | validationStatus: 'error',
343 | errorMsg: `Password is too long (Maximum ${PASSWORD_MAX_LENGTH} characters allowed.)`
344 | }
345 | } else {
346 | return {
347 | validateStatus: 'success',
348 | errorMsg: null,
349 | };
350 | }
351 | }
352 |
353 | }
354 |
355 | export default Signup;
--------------------------------------------------------------------------------
/polling-app-client/src/util/APIUtils.js:
--------------------------------------------------------------------------------
1 | import { API_BASE_URL, POLL_LIST_SIZE, ACCESS_TOKEN } from '../constants';
2 |
3 | const request = (options) => {
4 | const headers = new Headers({
5 | 'Content-Type': 'application/json',
6 | })
7 |
8 | if(localStorage.getItem(ACCESS_TOKEN)) {
9 | headers.append('Authorization', 'Bearer ' + localStorage.getItem(ACCESS_TOKEN))
10 | }
11 |
12 | const defaults = {headers: headers};
13 | options = Object.assign({}, defaults, options);
14 |
15 | return fetch(options.url, options)
16 | .then(response =>
17 | response.json().then(json => {
18 | if(!response.ok) {
19 | return Promise.reject(json);
20 | }
21 | return json;
22 | })
23 | );
24 | };
25 |
26 | export function getAllPolls(page, size) {
27 | page = page || 0;
28 | size = size || POLL_LIST_SIZE;
29 |
30 | return request({
31 | url: API_BASE_URL + "/polls?page=" + page + "&size=" + size,
32 | method: 'GET'
33 | });
34 | }
35 |
36 | export function createPoll(pollData) {
37 | return request({
38 | url: API_BASE_URL + "/polls",
39 | method: 'POST',
40 | body: JSON.stringify(pollData)
41 | });
42 | }
43 |
44 | export function castVote(voteData) {
45 | return request({
46 | url: API_BASE_URL + "/polls/" + voteData.pollId + "/votes",
47 | method: 'POST',
48 | body: JSON.stringify(voteData)
49 | });
50 | }
51 |
52 | export function login(loginRequest) {
53 | return request({
54 | url: API_BASE_URL + "/auth/signin",
55 | method: 'POST',
56 | body: JSON.stringify(loginRequest)
57 | });
58 | }
59 |
60 | export function signup(signupRequest) {
61 | return request({
62 | url: API_BASE_URL + "/auth/signup",
63 | method: 'POST',
64 | body: JSON.stringify(signupRequest)
65 | });
66 | }
67 |
68 | export function checkUsernameAvailability(username) {
69 | return request({
70 | url: API_BASE_URL + "/user/checkUsernameAvailability?username=" + username,
71 | method: 'GET'
72 | });
73 | }
74 |
75 | export function checkEmailAvailability(email) {
76 | return request({
77 | url: API_BASE_URL + "/user/checkEmailAvailability?email=" + email,
78 | method: 'GET'
79 | });
80 | }
81 |
82 |
83 | export function getCurrentUser() {
84 | if(!localStorage.getItem(ACCESS_TOKEN)) {
85 | return Promise.reject("No access token set.");
86 | }
87 |
88 | return request({
89 | url: API_BASE_URL + "/user/me",
90 | method: 'GET'
91 | });
92 | }
93 |
94 | export function getUserProfile(username) {
95 | return request({
96 | url: API_BASE_URL + "/users/" + username,
97 | method: 'GET'
98 | });
99 | }
100 |
101 | export function getUserCreatedPolls(username, page, size) {
102 | page = page || 0;
103 | size = size || POLL_LIST_SIZE;
104 |
105 | return request({
106 | url: API_BASE_URL + "/users/" + username + "/polls?page=" + page + "&size=" + size,
107 | method: 'GET'
108 | });
109 | }
110 |
111 | export function getUserVotedPolls(username, page, size) {
112 | page = page || 0;
113 | size = size || POLL_LIST_SIZE;
114 |
115 | return request({
116 | url: API_BASE_URL + "/users/" + username + "/votes?page=" + page + "&size=" + size,
117 | method: 'GET'
118 | });
119 | }
--------------------------------------------------------------------------------
/polling-app-client/src/util/Colors.js:
--------------------------------------------------------------------------------
1 | const colors = [
2 | '#F44336', '#e91e63', '#9c27b0', '#673ab7',
3 | '#ff9800', '#ff5722', '#795548', '#607d8b',
4 | '#3f51b5', '#2196F3', '#00bcd4', '#009688',
5 | '#2196F3', '#32c787', '#00BCD4', '#ff5652',
6 | '#ffc107', '#ff85af', '#FF9800', '#39bbb0',
7 | '#4CAF50', '#ffeb3b', '#ffc107',
8 | ];
9 |
10 | export function getAvatarColor(name) {
11 | name = name.substr(0, 6);
12 |
13 | var hash = 0;
14 | for (var i = 0; i < name.length; i++) {
15 | hash = 31 * hash + name.charCodeAt(i);
16 | }
17 | var index = Math.abs(hash % colors.length);
18 | return colors[index];
19 | }
--------------------------------------------------------------------------------
/polling-app-client/src/util/Helpers.js:
--------------------------------------------------------------------------------
1 |
2 | export function formatDate(dateString) {
3 | const date = new Date(dateString);
4 |
5 | const monthNames = [
6 | "January", "February", "March",
7 | "April", "May", "June", "July",
8 | "August", "September", "October",
9 | "November", "December"
10 | ];
11 |
12 | const monthIndex = date.getMonth();
13 | const year = date.getFullYear();
14 |
15 | return monthNames[monthIndex] + ' ' + year;
16 | }
17 |
18 | export function formatDateTime(dateTimeString) {
19 | const date = new Date(dateTimeString);
20 |
21 | const monthNames = [
22 | "Jan", "Feb", "Mar", "Apr",
23 | "May", "Jun", "Jul", "Aug",
24 | "Sep", "Oct", "Nov", "Dec"
25 | ];
26 |
27 | const monthIndex = date.getMonth();
28 | const year = date.getFullYear();
29 |
30 | return date.getDate() + ' ' + monthNames[monthIndex] + ' ' + year + ' - ' + date.getHours() + ':' + date.getMinutes();
31 | }
--------------------------------------------------------------------------------
/polling-app-server/.gitignore:
--------------------------------------------------------------------------------
1 | target/
2 | !.mvn/wrapper/maven-wrapper.jar
3 |
4 | ### STS ###
5 | .apt_generated
6 | .classpath
7 | .factorypath
8 | .project
9 | .settings
10 | .springBeans
11 |
12 | ### IntelliJ IDEA ###
13 | .idea
14 | *.iws
15 | *.iml
16 | *.ipr
17 |
18 | ### NetBeans ###
19 | nbproject/private/
20 | build/
21 | nbbuild/
22 | dist/
23 | nbdist/
24 | .nb-gradle/
25 | # Elastic Beanstalk Files
26 | .elasticbeanstalk/*
27 | !.elasticbeanstalk/*.cfg.yml
28 | !.elasticbeanstalk/*.global.yml
29 | .ebextensions
30 |
31 | src/main/resources/application-prod.properties
32 |
--------------------------------------------------------------------------------
/polling-app-server/.mvn/wrapper/maven-wrapper.jar:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/callicoder/spring-security-react-ant-design-polls-app/362fad90cab17e76453b3b9e273c594de6ee3d7f/polling-app-server/.mvn/wrapper/maven-wrapper.jar
--------------------------------------------------------------------------------
/polling-app-server/.mvn/wrapper/maven-wrapper.properties:
--------------------------------------------------------------------------------
1 | distributionUrl=https://repo1.maven.org/maven2/org/apache/maven/apache-maven/3.5.0/apache-maven-3.5.0-bin.zip
2 |
--------------------------------------------------------------------------------
/polling-app-server/Dockerfile:
--------------------------------------------------------------------------------
1 | #### Stage 1: Build the application
2 | FROM openjdk:8-jdk-alpine as build
3 |
4 | # Set the current working directory inside the image
5 | WORKDIR /app
6 |
7 | # Copy maven executable to the image
8 | COPY mvnw .
9 | COPY .mvn .mvn
10 |
11 | # Copy the pom.xml file
12 | COPY pom.xml .
13 |
14 | # Build all the dependencies in preparation to go offline.
15 | # This is a separate step so the dependencies will be cached unless
16 | # the pom.xml file has changed.
17 | RUN ./mvnw dependency:go-offline -B
18 |
19 | # Copy the project source
20 | COPY src src
21 |
22 | # Package the application
23 | RUN ./mvnw package -DskipTests
24 | RUN mkdir -p target/dependency && (cd target/dependency; jar -xf ../*.jar)
25 |
26 | #### Stage 2: A minimal docker image with command to run the app
27 | FROM openjdk:8-jre-alpine
28 |
29 | ARG DEPENDENCY=/app/target/dependency
30 |
31 | # Copy project dependencies from the build stage
32 | COPY --from=build ${DEPENDENCY}/BOOT-INF/lib /app/lib
33 | COPY --from=build ${DEPENDENCY}/META-INF /app/META-INF
34 | COPY --from=build ${DEPENDENCY}/BOOT-INF/classes /app
35 |
36 | ENTRYPOINT ["java","-cp","app:app/lib/*","com.example.polls.PollsApplication"]
--------------------------------------------------------------------------------
/polling-app-server/mvnw:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 | # ----------------------------------------------------------------------------
3 | # Licensed to the Apache Software Foundation (ASF) under one
4 | # or more contributor license agreements. See the NOTICE file
5 | # distributed with this work for additional information
6 | # regarding copyright ownership. The ASF licenses this file
7 | # to you under the Apache License, Version 2.0 (the
8 | # "License"); you may not use this file except in compliance
9 | # with the License. You may obtain a copy of the License at
10 | #
11 | # https://www.apache.org/licenses/LICENSE-2.0
12 | #
13 | # Unless required by applicable law or agreed to in writing,
14 | # software distributed under the License is distributed on an
15 | # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
16 | # KIND, either express or implied. See the License for the
17 | # specific language governing permissions and limitations
18 | # under the License.
19 | # ----------------------------------------------------------------------------
20 |
21 | # ----------------------------------------------------------------------------
22 | # Maven Start Up Batch script
23 | #
24 | # Required ENV vars:
25 | # ------------------
26 | # JAVA_HOME - location of a JDK home dir
27 | #
28 | # Optional ENV vars
29 | # -----------------
30 | # M2_HOME - location of maven2's installed home dir
31 | # MAVEN_OPTS - parameters passed to the Java VM when running Maven
32 | # e.g. to debug Maven itself, use
33 | # set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000
34 | # MAVEN_SKIP_RC - flag to disable loading of mavenrc files
35 | # ----------------------------------------------------------------------------
36 |
37 | if [ -z "$MAVEN_SKIP_RC" ] ; then
38 |
39 | if [ -f /etc/mavenrc ] ; then
40 | . /etc/mavenrc
41 | fi
42 |
43 | if [ -f "$HOME/.mavenrc" ] ; then
44 | . "$HOME/.mavenrc"
45 | fi
46 |
47 | fi
48 |
49 | # OS specific support. $var _must_ be set to either true or false.
50 | cygwin=false;
51 | darwin=false;
52 | mingw=false
53 | case "`uname`" in
54 | CYGWIN*) cygwin=true ;;
55 | MINGW*) mingw=true;;
56 | Darwin*) darwin=true
57 | # Use /usr/libexec/java_home if available, otherwise fall back to /Library/Java/Home
58 | # See https://developer.apple.com/library/mac/qa/qa1170/_index.html
59 | if [ -z "$JAVA_HOME" ]; then
60 | if [ -x "/usr/libexec/java_home" ]; then
61 | export JAVA_HOME="`/usr/libexec/java_home`"
62 | else
63 | export JAVA_HOME="/Library/Java/Home"
64 | fi
65 | fi
66 | ;;
67 | esac
68 |
69 | if [ -z "$JAVA_HOME" ] ; then
70 | if [ -r /etc/gentoo-release ] ; then
71 | JAVA_HOME=`java-config --jre-home`
72 | fi
73 | fi
74 |
75 | if [ -z "$M2_HOME" ] ; then
76 | ## resolve links - $0 may be a link to maven's home
77 | PRG="$0"
78 |
79 | # need this for relative symlinks
80 | while [ -h "$PRG" ] ; do
81 | ls=`ls -ld "$PRG"`
82 | link=`expr "$ls" : '.*-> \(.*\)$'`
83 | if expr "$link" : '/.*' > /dev/null; then
84 | PRG="$link"
85 | else
86 | PRG="`dirname "$PRG"`/$link"
87 | fi
88 | done
89 |
90 | saveddir=`pwd`
91 |
92 | M2_HOME=`dirname "$PRG"`/..
93 |
94 | # make it fully qualified
95 | M2_HOME=`cd "$M2_HOME" && pwd`
96 |
97 | cd "$saveddir"
98 | # echo Using m2 at $M2_HOME
99 | fi
100 |
101 | # For Cygwin, ensure paths are in UNIX format before anything is touched
102 | if $cygwin ; then
103 | [ -n "$M2_HOME" ] &&
104 | M2_HOME=`cygpath --unix "$M2_HOME"`
105 | [ -n "$JAVA_HOME" ] &&
106 | JAVA_HOME=`cygpath --unix "$JAVA_HOME"`
107 | [ -n "$CLASSPATH" ] &&
108 | CLASSPATH=`cygpath --path --unix "$CLASSPATH"`
109 | fi
110 |
111 | # For Mingw, ensure paths are in UNIX format before anything is touched
112 | if $mingw ; then
113 | [ -n "$M2_HOME" ] &&
114 | M2_HOME="`(cd "$M2_HOME"; pwd)`"
115 | [ -n "$JAVA_HOME" ] &&
116 | JAVA_HOME="`(cd "$JAVA_HOME"; pwd)`"
117 | fi
118 |
119 | if [ -z "$JAVA_HOME" ]; then
120 | javaExecutable="`which javac`"
121 | if [ -n "$javaExecutable" ] && ! [ "`expr \"$javaExecutable\" : '\([^ ]*\)'`" = "no" ]; then
122 | # readlink(1) is not available as standard on Solaris 10.
123 | readLink=`which readlink`
124 | if [ ! `expr "$readLink" : '\([^ ]*\)'` = "no" ]; then
125 | if $darwin ; then
126 | javaHome="`dirname \"$javaExecutable\"`"
127 | javaExecutable="`cd \"$javaHome\" && pwd -P`/javac"
128 | else
129 | javaExecutable="`readlink -f \"$javaExecutable\"`"
130 | fi
131 | javaHome="`dirname \"$javaExecutable\"`"
132 | javaHome=`expr "$javaHome" : '\(.*\)/bin'`
133 | JAVA_HOME="$javaHome"
134 | export JAVA_HOME
135 | fi
136 | fi
137 | fi
138 |
139 | if [ -z "$JAVACMD" ] ; then
140 | if [ -n "$JAVA_HOME" ] ; then
141 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
142 | # IBM's JDK on AIX uses strange locations for the executables
143 | JAVACMD="$JAVA_HOME/jre/sh/java"
144 | else
145 | JAVACMD="$JAVA_HOME/bin/java"
146 | fi
147 | else
148 | JAVACMD="`which java`"
149 | fi
150 | fi
151 |
152 | if [ ! -x "$JAVACMD" ] ; then
153 | echo "Error: JAVA_HOME is not defined correctly." >&2
154 | echo " We cannot execute $JAVACMD" >&2
155 | exit 1
156 | fi
157 |
158 | if [ -z "$JAVA_HOME" ] ; then
159 | echo "Warning: JAVA_HOME environment variable is not set."
160 | fi
161 |
162 | CLASSWORLDS_LAUNCHER=org.codehaus.plexus.classworlds.launcher.Launcher
163 |
164 | # traverses directory structure from process work directory to filesystem root
165 | # first directory with .mvn subdirectory is considered project base directory
166 | find_maven_basedir() {
167 |
168 | if [ -z "$1" ]
169 | then
170 | echo "Path not specified to find_maven_basedir"
171 | return 1
172 | fi
173 |
174 | basedir="$1"
175 | wdir="$1"
176 | while [ "$wdir" != '/' ] ; do
177 | if [ -d "$wdir"/.mvn ] ; then
178 | basedir=$wdir
179 | break
180 | fi
181 | # workaround for JBEAP-8937 (on Solaris 10/Sparc)
182 | if [ -d "${wdir}" ]; then
183 | wdir=`cd "$wdir/.."; pwd`
184 | fi
185 | # end of workaround
186 | done
187 | echo "${basedir}"
188 | }
189 |
190 | # concatenates all lines of a file
191 | concat_lines() {
192 | if [ -f "$1" ]; then
193 | echo "$(tr -s '\n' ' ' < "$1")"
194 | fi
195 | }
196 |
197 | BASE_DIR=`find_maven_basedir "$(pwd)"`
198 | if [ -z "$BASE_DIR" ]; then
199 | exit 1;
200 | fi
201 |
202 | ##########################################################################################
203 | # Extension to allow automatically downloading the maven-wrapper.jar from Maven-central
204 | # This allows using the maven wrapper in projects that prohibit checking in binary data.
205 | ##########################################################################################
206 | if [ -r "$BASE_DIR/.mvn/wrapper/maven-wrapper.jar" ]; then
207 | if [ "$MVNW_VERBOSE" = true ]; then
208 | echo "Found .mvn/wrapper/maven-wrapper.jar"
209 | fi
210 | else
211 | if [ "$MVNW_VERBOSE" = true ]; then
212 | echo "Couldn't find .mvn/wrapper/maven-wrapper.jar, downloading it ..."
213 | fi
214 | if [ -n "$MVNW_REPOURL" ]; then
215 | jarUrl="$MVNW_REPOURL/io/takari/maven-wrapper/0.5.6/maven-wrapper-0.5.6.jar"
216 | else
217 | jarUrl="https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/0.5.6/maven-wrapper-0.5.6.jar"
218 | fi
219 | while IFS="=" read key value; do
220 | case "$key" in (wrapperUrl) jarUrl="$value"; break ;;
221 | esac
222 | done < "$BASE_DIR/.mvn/wrapper/maven-wrapper.properties"
223 | if [ "$MVNW_VERBOSE" = true ]; then
224 | echo "Downloading from: $jarUrl"
225 | fi
226 | wrapperJarPath="$BASE_DIR/.mvn/wrapper/maven-wrapper.jar"
227 | if $cygwin; then
228 | wrapperJarPath=`cygpath --path --windows "$wrapperJarPath"`
229 | fi
230 |
231 | if command -v wget > /dev/null; then
232 | if [ "$MVNW_VERBOSE" = true ]; then
233 | echo "Found wget ... using wget"
234 | fi
235 | if [ -z "$MVNW_USERNAME" ] || [ -z "$MVNW_PASSWORD" ]; then
236 | wget "$jarUrl" -O "$wrapperJarPath"
237 | else
238 | wget --http-user=$MVNW_USERNAME --http-password=$MVNW_PASSWORD "$jarUrl" -O "$wrapperJarPath"
239 | fi
240 | elif command -v curl > /dev/null; then
241 | if [ "$MVNW_VERBOSE" = true ]; then
242 | echo "Found curl ... using curl"
243 | fi
244 | if [ -z "$MVNW_USERNAME" ] || [ -z "$MVNW_PASSWORD" ]; then
245 | curl -o "$wrapperJarPath" "$jarUrl" -f
246 | else
247 | curl --user $MVNW_USERNAME:$MVNW_PASSWORD -o "$wrapperJarPath" "$jarUrl" -f
248 | fi
249 |
250 | else
251 | if [ "$MVNW_VERBOSE" = true ]; then
252 | echo "Falling back to using Java to download"
253 | fi
254 | javaClass="$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.java"
255 | # For Cygwin, switch paths to Windows format before running javac
256 | if $cygwin; then
257 | javaClass=`cygpath --path --windows "$javaClass"`
258 | fi
259 | if [ -e "$javaClass" ]; then
260 | if [ ! -e "$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.class" ]; then
261 | if [ "$MVNW_VERBOSE" = true ]; then
262 | echo " - Compiling MavenWrapperDownloader.java ..."
263 | fi
264 | # Compiling the Java class
265 | ("$JAVA_HOME/bin/javac" "$javaClass")
266 | fi
267 | if [ -e "$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.class" ]; then
268 | # Running the downloader
269 | if [ "$MVNW_VERBOSE" = true ]; then
270 | echo " - Running MavenWrapperDownloader.java ..."
271 | fi
272 | ("$JAVA_HOME/bin/java" -cp .mvn/wrapper MavenWrapperDownloader "$MAVEN_PROJECTBASEDIR")
273 | fi
274 | fi
275 | fi
276 | fi
277 | ##########################################################################################
278 | # End of extension
279 | ##########################################################################################
280 |
281 | export MAVEN_PROJECTBASEDIR=${MAVEN_BASEDIR:-"$BASE_DIR"}
282 | if [ "$MVNW_VERBOSE" = true ]; then
283 | echo $MAVEN_PROJECTBASEDIR
284 | fi
285 | MAVEN_OPTS="$(concat_lines "$MAVEN_PROJECTBASEDIR/.mvn/jvm.config") $MAVEN_OPTS"
286 |
287 | # For Cygwin, switch paths to Windows format before running java
288 | if $cygwin; then
289 | [ -n "$M2_HOME" ] &&
290 | M2_HOME=`cygpath --path --windows "$M2_HOME"`
291 | [ -n "$JAVA_HOME" ] &&
292 | JAVA_HOME=`cygpath --path --windows "$JAVA_HOME"`
293 | [ -n "$CLASSPATH" ] &&
294 | CLASSPATH=`cygpath --path --windows "$CLASSPATH"`
295 | [ -n "$MAVEN_PROJECTBASEDIR" ] &&
296 | MAVEN_PROJECTBASEDIR=`cygpath --path --windows "$MAVEN_PROJECTBASEDIR"`
297 | fi
298 |
299 | # Provide a "standardized" way to retrieve the CLI args that will
300 | # work with both Windows and non-Windows executions.
301 | MAVEN_CMD_LINE_ARGS="$MAVEN_CONFIG $@"
302 | export MAVEN_CMD_LINE_ARGS
303 |
304 | WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain
305 |
306 | exec "$JAVACMD" \
307 | $MAVEN_OPTS \
308 | -classpath "$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.jar" \
309 | "-Dmaven.home=${M2_HOME}" "-Dmaven.multiModuleProjectDirectory=${MAVEN_PROJECTBASEDIR}" \
310 | ${WRAPPER_LAUNCHER} $MAVEN_CONFIG "$@"
311 |
--------------------------------------------------------------------------------
/polling-app-server/mvnw.cmd:
--------------------------------------------------------------------------------
1 | @REM ----------------------------------------------------------------------------
2 | @REM Licensed to the Apache Software Foundation (ASF) under one
3 | @REM or more contributor license agreements. See the NOTICE file
4 | @REM distributed with this work for additional information
5 | @REM regarding copyright ownership. The ASF licenses this file
6 | @REM to you under the Apache License, Version 2.0 (the
7 | @REM "License"); you may not use this file except in compliance
8 | @REM with the License. You may obtain a copy of the License at
9 | @REM
10 | @REM https://www.apache.org/licenses/LICENSE-2.0
11 | @REM
12 | @REM Unless required by applicable law or agreed to in writing,
13 | @REM software distributed under the License is distributed on an
14 | @REM "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15 | @REM KIND, either express or implied. See the License for the
16 | @REM specific language governing permissions and limitations
17 | @REM under the License.
18 | @REM ----------------------------------------------------------------------------
19 |
20 | @REM ----------------------------------------------------------------------------
21 | @REM Maven Start Up Batch script
22 | @REM
23 | @REM Required ENV vars:
24 | @REM JAVA_HOME - location of a JDK home dir
25 | @REM
26 | @REM Optional ENV vars
27 | @REM M2_HOME - location of maven2's installed home dir
28 | @REM MAVEN_BATCH_ECHO - set to 'on' to enable the echoing of the batch commands
29 | @REM MAVEN_BATCH_PAUSE - set to 'on' to wait for a keystroke before ending
30 | @REM MAVEN_OPTS - parameters passed to the Java VM when running Maven
31 | @REM e.g. to debug Maven itself, use
32 | @REM set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000
33 | @REM MAVEN_SKIP_RC - flag to disable loading of mavenrc files
34 | @REM ----------------------------------------------------------------------------
35 |
36 | @REM Begin all REM lines with '@' in case MAVEN_BATCH_ECHO is 'on'
37 | @echo off
38 | @REM set title of command window
39 | title %0
40 | @REM enable echoing by setting MAVEN_BATCH_ECHO to 'on'
41 | @if "%MAVEN_BATCH_ECHO%" == "on" echo %MAVEN_BATCH_ECHO%
42 |
43 | @REM set %HOME% to equivalent of $HOME
44 | if "%HOME%" == "" (set "HOME=%HOMEDRIVE%%HOMEPATH%")
45 |
46 | @REM Execute a user defined script before this one
47 | if not "%MAVEN_SKIP_RC%" == "" goto skipRcPre
48 | @REM check for pre script, once with legacy .bat ending and once with .cmd ending
49 | if exist "%HOME%\mavenrc_pre.bat" call "%HOME%\mavenrc_pre.bat"
50 | if exist "%HOME%\mavenrc_pre.cmd" call "%HOME%\mavenrc_pre.cmd"
51 | :skipRcPre
52 |
53 | @setlocal
54 |
55 | set ERROR_CODE=0
56 |
57 | @REM To isolate internal variables from possible post scripts, we use another setlocal
58 | @setlocal
59 |
60 | @REM ==== START VALIDATION ====
61 | if not "%JAVA_HOME%" == "" goto OkJHome
62 |
63 | echo.
64 | echo Error: JAVA_HOME not found in your environment. >&2
65 | echo Please set the JAVA_HOME variable in your environment to match the >&2
66 | echo location of your Java installation. >&2
67 | echo.
68 | goto error
69 |
70 | :OkJHome
71 | if exist "%JAVA_HOME%\bin\java.exe" goto init
72 |
73 | echo.
74 | echo Error: JAVA_HOME is set to an invalid directory. >&2
75 | echo JAVA_HOME = "%JAVA_HOME%" >&2
76 | echo Please set the JAVA_HOME variable in your environment to match the >&2
77 | echo location of your Java installation. >&2
78 | echo.
79 | goto error
80 |
81 | @REM ==== END VALIDATION ====
82 |
83 | :init
84 |
85 | @REM Find the project base dir, i.e. the directory that contains the folder ".mvn".
86 | @REM Fallback to current working directory if not found.
87 |
88 | set MAVEN_PROJECTBASEDIR=%MAVEN_BASEDIR%
89 | IF NOT "%MAVEN_PROJECTBASEDIR%"=="" goto endDetectBaseDir
90 |
91 | set EXEC_DIR=%CD%
92 | set WDIR=%EXEC_DIR%
93 | :findBaseDir
94 | IF EXIST "%WDIR%"\.mvn goto baseDirFound
95 | cd ..
96 | IF "%WDIR%"=="%CD%" goto baseDirNotFound
97 | set WDIR=%CD%
98 | goto findBaseDir
99 |
100 | :baseDirFound
101 | set MAVEN_PROJECTBASEDIR=%WDIR%
102 | cd "%EXEC_DIR%"
103 | goto endDetectBaseDir
104 |
105 | :baseDirNotFound
106 | set MAVEN_PROJECTBASEDIR=%EXEC_DIR%
107 | cd "%EXEC_DIR%"
108 |
109 | :endDetectBaseDir
110 |
111 | IF NOT EXIST "%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config" goto endReadAdditionalConfig
112 |
113 | @setlocal EnableExtensions EnableDelayedExpansion
114 | for /F "usebackq delims=" %%a in ("%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config") do set JVM_CONFIG_MAVEN_PROPS=!JVM_CONFIG_MAVEN_PROPS! %%a
115 | @endlocal & set JVM_CONFIG_MAVEN_PROPS=%JVM_CONFIG_MAVEN_PROPS%
116 |
117 | :endReadAdditionalConfig
118 |
119 | SET MAVEN_JAVA_EXE="%JAVA_HOME%\bin\java.exe"
120 | set WRAPPER_JAR="%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.jar"
121 | set WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain
122 |
123 | set DOWNLOAD_URL="https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/0.5.6/maven-wrapper-0.5.6.jar"
124 |
125 | FOR /F "tokens=1,2 delims==" %%A IN ("%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.properties") DO (
126 | IF "%%A"=="wrapperUrl" SET DOWNLOAD_URL=%%B
127 | )
128 |
129 | @REM Extension to allow automatically downloading the maven-wrapper.jar from Maven-central
130 | @REM This allows using the maven wrapper in projects that prohibit checking in binary data.
131 | if exist %WRAPPER_JAR% (
132 | if "%MVNW_VERBOSE%" == "true" (
133 | echo Found %WRAPPER_JAR%
134 | )
135 | ) else (
136 | if not "%MVNW_REPOURL%" == "" (
137 | SET DOWNLOAD_URL="%MVNW_REPOURL%/io/takari/maven-wrapper/0.5.6/maven-wrapper-0.5.6.jar"
138 | )
139 | if "%MVNW_VERBOSE%" == "true" (
140 | echo Couldn't find %WRAPPER_JAR%, downloading it ...
141 | echo Downloading from: %DOWNLOAD_URL%
142 | )
143 |
144 | powershell -Command "&{"^
145 | "$webclient = new-object System.Net.WebClient;"^
146 | "if (-not ([string]::IsNullOrEmpty('%MVNW_USERNAME%') -and [string]::IsNullOrEmpty('%MVNW_PASSWORD%'))) {"^
147 | "$webclient.Credentials = new-object System.Net.NetworkCredential('%MVNW_USERNAME%', '%MVNW_PASSWORD%');"^
148 | "}"^
149 | "[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12; $webclient.DownloadFile('%DOWNLOAD_URL%', '%WRAPPER_JAR%')"^
150 | "}"
151 | if "%MVNW_VERBOSE%" == "true" (
152 | echo Finished downloading %WRAPPER_JAR%
153 | )
154 | )
155 | @REM End of extension
156 |
157 | @REM Provide a "standardized" way to retrieve the CLI args that will
158 | @REM work with both Windows and non-Windows executions.
159 | set MAVEN_CMD_LINE_ARGS=%*
160 |
161 | %MAVEN_JAVA_EXE% %JVM_CONFIG_MAVEN_PROPS% %MAVEN_OPTS% %MAVEN_DEBUG_OPTS% -classpath %WRAPPER_JAR% "-Dmaven.multiModuleProjectDirectory=%MAVEN_PROJECTBASEDIR%" %WRAPPER_LAUNCHER% %MAVEN_CONFIG% %*
162 | if ERRORLEVEL 1 goto error
163 | goto end
164 |
165 | :error
166 | set ERROR_CODE=1
167 |
168 | :end
169 | @endlocal & set ERROR_CODE=%ERROR_CODE%
170 |
171 | if not "%MAVEN_SKIP_RC%" == "" goto skipRcPost
172 | @REM check for post script, once with legacy .bat ending and once with .cmd ending
173 | if exist "%HOME%\mavenrc_post.bat" call "%HOME%\mavenrc_post.bat"
174 | if exist "%HOME%\mavenrc_post.cmd" call "%HOME%\mavenrc_post.cmd"
175 | :skipRcPost
176 |
177 | @REM pause the script if MAVEN_BATCH_PAUSE is set to 'on'
178 | if "%MAVEN_BATCH_PAUSE%" == "on" pause
179 |
180 | if "%MAVEN_TERMINATE_CMD%" == "on" exit %ERROR_CODE%
181 |
182 | exit /B %ERROR_CODE%
183 |
--------------------------------------------------------------------------------
/polling-app-server/pom.xml:
--------------------------------------------------------------------------------
1 |
2 |
4 | 4.0.0
5 |
6 | com.example
7 | polls
8 | 0.0.1-SNAPSHOT
9 | jar
10 |
11 | polls
12 | Polling App built with Spring Boot, Spring Security, MySQL and JWT
13 |
14 |
15 | org.springframework.boot
16 | spring-boot-starter-parent
17 | 2.5.5
18 |
19 |
20 |
21 |
22 | UTF-8
23 | UTF-8
24 | 11
25 | 0.11.2
26 |
27 |
28 |
29 |
30 | org.springframework.boot
31 | spring-boot-starter-data-jpa
32 |
33 |
34 | org.springframework.boot
35 | spring-boot-starter-security
36 |
37 |
38 | org.springframework.boot
39 | spring-boot-starter-web
40 |
41 |
42 | org.springframework.boot
43 | spring-boot-starter-validation
44 |
45 |
46 | mysql
47 | mysql-connector-java
48 | runtime
49 |
50 |
51 |
52 |
53 | io.jsonwebtoken
54 | jjwt-api
55 | ${jjwt.version}
56 |
57 |
58 | io.jsonwebtoken
59 | jjwt-impl
60 | ${jjwt.version}
61 | runtime
62 |
63 |
64 | io.jsonwebtoken
65 | jjwt-jackson
66 | ${jjwt.version}
67 | runtime
68 |
69 |
70 |
71 |
72 | com.fasterxml.jackson.datatype
73 | jackson-datatype-jsr310
74 |
75 |
76 |
77 | org.springframework.boot
78 | spring-boot-starter-test
79 | test
80 |
81 |
82 |
83 | org.springframework.security
84 | spring-security-test
85 | test
86 |
87 |
88 |
89 |
90 |
91 |
92 | org.springframework.boot
93 | spring-boot-maven-plugin
94 |
95 |
96 |
97 |
98 |
99 |
--------------------------------------------------------------------------------
/polling-app-server/src/main/java/com/example/polls/PollsApplication.java:
--------------------------------------------------------------------------------
1 | package com.example.polls;
2 |
3 | import org.springframework.boot.SpringApplication;
4 | import org.springframework.boot.autoconfigure.SpringBootApplication;
5 | import org.springframework.boot.autoconfigure.domain.EntityScan;
6 | import org.springframework.data.jpa.convert.threeten.Jsr310JpaConverters;
7 |
8 | import javax.annotation.PostConstruct;
9 | import java.util.TimeZone;
10 |
11 | @SpringBootApplication
12 | @EntityScan(basePackageClasses = {
13 | PollsApplication.class,
14 | Jsr310JpaConverters.class
15 | })
16 | public class PollsApplication {
17 |
18 | @PostConstruct
19 | void init() {
20 | TimeZone.setDefault(TimeZone.getTimeZone("UTC"));
21 | }
22 |
23 | public static void main(String[] args) {
24 | SpringApplication.run(PollsApplication.class, args);
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/polling-app-server/src/main/java/com/example/polls/config/AuditingConfig.java:
--------------------------------------------------------------------------------
1 | package com.example.polls.config;
2 |
3 | import com.example.polls.security.UserPrincipal;
4 | import org.springframework.context.annotation.Bean;
5 | import org.springframework.context.annotation.Configuration;
6 | import org.springframework.data.domain.AuditorAware;
7 | import org.springframework.data.jpa.repository.config.EnableJpaAuditing;
8 | import org.springframework.security.authentication.AnonymousAuthenticationToken;
9 | import org.springframework.security.core.Authentication;
10 | import org.springframework.security.core.context.SecurityContextHolder;
11 |
12 | import java.util.Optional;
13 |
14 | @Configuration
15 | @EnableJpaAuditing
16 | public class AuditingConfig {
17 |
18 | @Bean
19 | public AuditorAware auditorProvider() {
20 | return new SpringSecurityAuditAwareImpl();
21 | }
22 | }
23 |
24 | class SpringSecurityAuditAwareImpl implements AuditorAware {
25 |
26 | @Override
27 | public Optional getCurrentAuditor() {
28 | Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
29 |
30 | if (authentication == null ||
31 | !authentication.isAuthenticated() ||
32 | authentication instanceof AnonymousAuthenticationToken) {
33 | return Optional.empty();
34 | }
35 |
36 | UserPrincipal userPrincipal = (UserPrincipal) authentication.getPrincipal();
37 |
38 | return Optional.ofNullable(userPrincipal.getId());
39 | }
40 | }
41 |
--------------------------------------------------------------------------------
/polling-app-server/src/main/java/com/example/polls/config/SecurityConfig.java:
--------------------------------------------------------------------------------
1 | package com.example.polls.config;
2 |
3 | import com.example.polls.security.CustomUserDetailsService;
4 | import com.example.polls.security.JwtAuthenticationEntryPoint;
5 | import com.example.polls.security.JwtAuthenticationFilter;
6 | import org.springframework.beans.factory.annotation.Autowired;
7 | import org.springframework.context.annotation.Bean;
8 | import org.springframework.context.annotation.Configuration;
9 | import org.springframework.http.HttpMethod;
10 | import org.springframework.security.authentication.AuthenticationManager;
11 | import org.springframework.security.config.BeanIds;
12 | import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
13 | import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
14 | import org.springframework.security.config.annotation.web.builders.HttpSecurity;
15 | import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
16 | import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
17 | import org.springframework.security.config.http.SessionCreationPolicy;
18 | import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
19 | import org.springframework.security.crypto.password.PasswordEncoder;
20 | import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
21 |
22 | @Configuration
23 | @EnableWebSecurity
24 | @EnableGlobalMethodSecurity(
25 | securedEnabled = true,
26 | jsr250Enabled = true,
27 | prePostEnabled = true
28 | )
29 | public class SecurityConfig extends WebSecurityConfigurerAdapter {
30 |
31 | @Autowired
32 | CustomUserDetailsService customUserDetailsService;
33 |
34 | @Autowired
35 | private JwtAuthenticationEntryPoint unauthorizedHandler;
36 |
37 | @Bean
38 | public JwtAuthenticationFilter jwtAuthenticationFilter() {
39 | return new JwtAuthenticationFilter();
40 | }
41 |
42 | @Override
43 | public void configure(AuthenticationManagerBuilder authenticationManagerBuilder) throws Exception {
44 | authenticationManagerBuilder
45 | .userDetailsService(customUserDetailsService)
46 | .passwordEncoder(passwordEncoder());
47 | }
48 |
49 | @Bean(BeanIds.AUTHENTICATION_MANAGER)
50 | @Override
51 | public AuthenticationManager authenticationManagerBean() throws Exception {
52 | return super.authenticationManagerBean();
53 | }
54 |
55 | @Bean
56 | public PasswordEncoder passwordEncoder() {
57 | return new BCryptPasswordEncoder();
58 | }
59 |
60 | @Override
61 | protected void configure(HttpSecurity http) throws Exception {
62 | http
63 | .cors()
64 | .and()
65 | .csrf()
66 | .disable()
67 | .exceptionHandling()
68 | .authenticationEntryPoint(unauthorizedHandler)
69 | .and()
70 | .sessionManagement()
71 | .sessionCreationPolicy(SessionCreationPolicy.STATELESS)
72 | .and()
73 | .authorizeRequests()
74 | .antMatchers("/",
75 | "/favicon.ico",
76 | "/**/*.png",
77 | "/**/*.gif",
78 | "/**/*.svg",
79 | "/**/*.jpg",
80 | "/**/*.html",
81 | "/**/*.css",
82 | "/**/*.js")
83 | .permitAll()
84 | .antMatchers("/api/auth/**")
85 | .permitAll()
86 | .antMatchers("/api/user/checkUsernameAvailability", "/api/user/checkEmailAvailability")
87 | .permitAll()
88 | .antMatchers(HttpMethod.GET, "/api/polls/**", "/api/users/**")
89 | .permitAll()
90 | .anyRequest()
91 | .authenticated();
92 |
93 | // Add our custom JWT security filter
94 | http.addFilterBefore(jwtAuthenticationFilter(), UsernamePasswordAuthenticationFilter.class);
95 |
96 | }
97 | }
--------------------------------------------------------------------------------
/polling-app-server/src/main/java/com/example/polls/config/WebMvcConfig.java:
--------------------------------------------------------------------------------
1 | package com.example.polls.config;
2 |
3 | import org.springframework.beans.factory.annotation.Value;
4 | import org.springframework.context.annotation.Configuration;
5 | import org.springframework.web.servlet.config.annotation.CorsRegistry;
6 | import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
7 |
8 | @Configuration
9 | public class WebMvcConfig implements WebMvcConfigurer {
10 |
11 | private final long MAX_AGE_SECS = 3600;
12 |
13 | @Value("${app.cors.allowedOrigins}")
14 | private String[] allowedOrigins;
15 |
16 | @Override
17 | public void addCorsMappings(CorsRegistry registry) {
18 | registry.addMapping("/**")
19 | .allowedOrigins(allowedOrigins)
20 | .allowedMethods("HEAD", "OPTIONS", "GET", "POST", "PUT", "PATCH", "DELETE")
21 | .maxAge(MAX_AGE_SECS);
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/polling-app-server/src/main/java/com/example/polls/controller/AuthController.java:
--------------------------------------------------------------------------------
1 | package com.example.polls.controller;
2 |
3 | import com.example.polls.exception.AppException;
4 | import com.example.polls.model.Role;
5 | import com.example.polls.model.RoleName;
6 | import com.example.polls.model.User;
7 | import com.example.polls.payload.ApiResponse;
8 | import com.example.polls.payload.JwtAuthenticationResponse;
9 | import com.example.polls.payload.LoginRequest;
10 | import com.example.polls.payload.SignUpRequest;
11 | import com.example.polls.repository.RoleRepository;
12 | import com.example.polls.repository.UserRepository;
13 | import com.example.polls.security.JwtTokenProvider;
14 | import org.springframework.beans.factory.annotation.Autowired;
15 | import org.springframework.http.HttpStatus;
16 | import org.springframework.http.ResponseEntity;
17 | import org.springframework.security.authentication.AuthenticationManager;
18 | import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
19 | import org.springframework.security.core.Authentication;
20 | import org.springframework.security.core.context.SecurityContextHolder;
21 | import org.springframework.security.crypto.password.PasswordEncoder;
22 | import org.springframework.web.bind.annotation.PostMapping;
23 | import org.springframework.web.bind.annotation.RequestBody;
24 | import org.springframework.web.bind.annotation.RequestMapping;
25 | import org.springframework.web.bind.annotation.RestController;
26 | import org.springframework.web.servlet.support.ServletUriComponentsBuilder;
27 |
28 | import javax.validation.Valid;
29 | import java.net.URI;
30 | import java.util.Collections;
31 |
32 | @RestController
33 | @RequestMapping("/api/auth")
34 | public class AuthController {
35 |
36 | @Autowired
37 | AuthenticationManager authenticationManager;
38 |
39 | @Autowired
40 | UserRepository userRepository;
41 |
42 | @Autowired
43 | RoleRepository roleRepository;
44 |
45 | @Autowired
46 | PasswordEncoder passwordEncoder;
47 |
48 | @Autowired
49 | JwtTokenProvider tokenProvider;
50 |
51 | @PostMapping("/signin")
52 | public ResponseEntity> authenticateUser(@Valid @RequestBody LoginRequest loginRequest) {
53 |
54 | Authentication authentication = authenticationManager.authenticate(
55 | new UsernamePasswordAuthenticationToken(
56 | loginRequest.getUsernameOrEmail(),
57 | loginRequest.getPassword()
58 | )
59 | );
60 |
61 | SecurityContextHolder.getContext().setAuthentication(authentication);
62 |
63 | String jwt = tokenProvider.generateToken(authentication);
64 | return ResponseEntity.ok(new JwtAuthenticationResponse(jwt));
65 | }
66 |
67 | @PostMapping("/signup")
68 | public ResponseEntity> registerUser(@Valid @RequestBody SignUpRequest signUpRequest) {
69 | if(userRepository.existsByUsername(signUpRequest.getUsername())) {
70 | return new ResponseEntity(new ApiResponse(false, "Username is already taken!"),
71 | HttpStatus.BAD_REQUEST);
72 | }
73 |
74 | if(userRepository.existsByEmail(signUpRequest.getEmail())) {
75 | return new ResponseEntity(new ApiResponse(false, "Email Address already in use!"),
76 | HttpStatus.BAD_REQUEST);
77 | }
78 |
79 | // Creating user's account
80 | User user = new User(signUpRequest.getName(), signUpRequest.getUsername(),
81 | signUpRequest.getEmail(), signUpRequest.getPassword());
82 |
83 | user.setPassword(passwordEncoder.encode(user.getPassword()));
84 |
85 | Role userRole = roleRepository.findByName(RoleName.ROLE_USER)
86 | .orElseThrow(() -> new AppException("User Role not set."));
87 |
88 | user.setRoles(Collections.singleton(userRole));
89 |
90 | User result = userRepository.save(user);
91 |
92 | URI location = ServletUriComponentsBuilder
93 | .fromCurrentContextPath().path("/users/{username}")
94 | .buildAndExpand(result.getUsername()).toUri();
95 |
96 | return ResponseEntity.created(location).body(new ApiResponse(true, "User registered successfully"));
97 | }
98 | }
99 |
--------------------------------------------------------------------------------
/polling-app-server/src/main/java/com/example/polls/controller/PollController.java:
--------------------------------------------------------------------------------
1 | package com.example.polls.controller;
2 |
3 | import com.example.polls.model.*;
4 | import com.example.polls.payload.*;
5 | import com.example.polls.repository.PollRepository;
6 | import com.example.polls.repository.UserRepository;
7 | import com.example.polls.repository.VoteRepository;
8 | import com.example.polls.security.CurrentUser;
9 | import com.example.polls.security.UserPrincipal;
10 | import com.example.polls.service.PollService;
11 | import com.example.polls.util.AppConstants;
12 | import org.slf4j.Logger;
13 | import org.slf4j.LoggerFactory;
14 | import org.springframework.beans.factory.annotation.Autowired;
15 | import org.springframework.http.ResponseEntity;
16 | import org.springframework.security.access.prepost.PreAuthorize;
17 | import org.springframework.web.bind.annotation.*;
18 | import org.springframework.web.servlet.support.ServletUriComponentsBuilder;
19 | import javax.validation.Valid;
20 | import java.net.URI;
21 |
22 | @RestController
23 | @RequestMapping("/api/polls")
24 | public class PollController {
25 |
26 | @Autowired
27 | private PollRepository pollRepository;
28 |
29 | @Autowired
30 | private VoteRepository voteRepository;
31 |
32 | @Autowired
33 | private UserRepository userRepository;
34 |
35 | @Autowired
36 | private PollService pollService;
37 |
38 | private static final Logger logger = LoggerFactory.getLogger(PollController.class);
39 |
40 | @GetMapping
41 | public PagedResponse getPolls(@CurrentUser UserPrincipal currentUser,
42 | @RequestParam(value = "page", defaultValue = AppConstants.DEFAULT_PAGE_NUMBER) int page,
43 | @RequestParam(value = "size", defaultValue = AppConstants.DEFAULT_PAGE_SIZE) int size) {
44 | return pollService.getAllPolls(currentUser, page, size);
45 | }
46 |
47 | @PostMapping
48 | @PreAuthorize("hasRole('USER')")
49 | public ResponseEntity> createPoll(@Valid @RequestBody PollRequest pollRequest) {
50 | Poll poll = pollService.createPoll(pollRequest);
51 |
52 | URI location = ServletUriComponentsBuilder
53 | .fromCurrentRequest().path("/{pollId}")
54 | .buildAndExpand(poll.getId()).toUri();
55 |
56 | return ResponseEntity.created(location)
57 | .body(new ApiResponse(true, "Poll Created Successfully"));
58 | }
59 |
60 | @GetMapping("/{pollId}")
61 | public PollResponse getPollById(@CurrentUser UserPrincipal currentUser,
62 | @PathVariable Long pollId) {
63 | return pollService.getPollById(pollId, currentUser);
64 | }
65 |
66 | @PostMapping("/{pollId}/votes")
67 | @PreAuthorize("hasRole('USER')")
68 | public PollResponse castVote(@CurrentUser UserPrincipal currentUser,
69 | @PathVariable Long pollId,
70 | @Valid @RequestBody VoteRequest voteRequest) {
71 | return pollService.castVoteAndGetUpdatedPoll(pollId, voteRequest, currentUser);
72 | }
73 |
74 | }
75 |
--------------------------------------------------------------------------------
/polling-app-server/src/main/java/com/example/polls/controller/UserController.java:
--------------------------------------------------------------------------------
1 | package com.example.polls.controller;
2 |
3 | import com.example.polls.exception.ResourceNotFoundException;
4 | import com.example.polls.model.User;
5 | import com.example.polls.payload.*;
6 | import com.example.polls.repository.PollRepository;
7 | import com.example.polls.repository.UserRepository;
8 | import com.example.polls.repository.VoteRepository;
9 | import com.example.polls.security.UserPrincipal;
10 | import com.example.polls.service.PollService;
11 | import com.example.polls.security.CurrentUser;
12 | import com.example.polls.util.AppConstants;
13 | import org.slf4j.Logger;
14 | import org.slf4j.LoggerFactory;
15 | import org.springframework.beans.factory.annotation.Autowired;
16 | import org.springframework.security.access.prepost.PreAuthorize;
17 | import org.springframework.web.bind.annotation.*;
18 |
19 | @RestController
20 | @RequestMapping("/api")
21 | public class UserController {
22 |
23 | @Autowired
24 | private UserRepository userRepository;
25 |
26 | @Autowired
27 | private PollRepository pollRepository;
28 |
29 | @Autowired
30 | private VoteRepository voteRepository;
31 |
32 | @Autowired
33 | private PollService pollService;
34 |
35 | private static final Logger logger = LoggerFactory.getLogger(UserController.class);
36 |
37 | @GetMapping("/user/me")
38 | @PreAuthorize("hasRole('USER')")
39 | public UserSummary getCurrentUser(@CurrentUser UserPrincipal currentUser) {
40 | UserSummary userSummary = new UserSummary(currentUser.getId(), currentUser.getUsername(), currentUser.getName());
41 | return userSummary;
42 | }
43 |
44 | @GetMapping("/user/checkUsernameAvailability")
45 | public UserIdentityAvailability checkUsernameAvailability(@RequestParam(value = "username") String username) {
46 | Boolean isAvailable = !userRepository.existsByUsername(username);
47 | return new UserIdentityAvailability(isAvailable);
48 | }
49 |
50 | @GetMapping("/user/checkEmailAvailability")
51 | public UserIdentityAvailability checkEmailAvailability(@RequestParam(value = "email") String email) {
52 | Boolean isAvailable = !userRepository.existsByEmail(email);
53 | return new UserIdentityAvailability(isAvailable);
54 | }
55 |
56 | @GetMapping("/users/{username}")
57 | public UserProfile getUserProfile(@PathVariable(value = "username") String username) {
58 | User user = userRepository.findByUsername(username)
59 | .orElseThrow(() -> new ResourceNotFoundException("User", "username", username));
60 |
61 | long pollCount = pollRepository.countByCreatedBy(user.getId());
62 | long voteCount = voteRepository.countByUserId(user.getId());
63 |
64 | UserProfile userProfile = new UserProfile(user.getId(), user.getUsername(), user.getName(), user.getCreatedAt(), pollCount, voteCount);
65 |
66 | return userProfile;
67 | }
68 |
69 | @GetMapping("/users/{username}/polls")
70 | public PagedResponse getPollsCreatedBy(@PathVariable(value = "username") String username,
71 | @CurrentUser UserPrincipal currentUser,
72 | @RequestParam(value = "page", defaultValue = AppConstants.DEFAULT_PAGE_NUMBER) int page,
73 | @RequestParam(value = "size", defaultValue = AppConstants.DEFAULT_PAGE_SIZE) int size) {
74 | return pollService.getPollsCreatedBy(username, currentUser, page, size);
75 | }
76 |
77 | @GetMapping("/users/{username}/votes")
78 | public PagedResponse getPollsVotedBy(@PathVariable(value = "username") String username,
79 | @CurrentUser UserPrincipal currentUser,
80 | @RequestParam(value = "page", defaultValue = AppConstants.DEFAULT_PAGE_NUMBER) int page,
81 | @RequestParam(value = "size", defaultValue = AppConstants.DEFAULT_PAGE_SIZE) int size) {
82 | return pollService.getPollsVotedBy(username, currentUser, page, size);
83 | }
84 |
85 | }
86 |
--------------------------------------------------------------------------------
/polling-app-server/src/main/java/com/example/polls/exception/AppException.java:
--------------------------------------------------------------------------------
1 | package com.example.polls.exception;
2 |
3 | import org.springframework.http.HttpStatus;
4 | import org.springframework.web.bind.annotation.ResponseStatus;
5 |
6 | @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
7 | public class AppException extends RuntimeException {
8 | public AppException(String message) {
9 | super(message);
10 | }
11 |
12 | public AppException(String message, Throwable cause) {
13 | super(message, cause);
14 | }
15 | }
16 |
--------------------------------------------------------------------------------
/polling-app-server/src/main/java/com/example/polls/exception/BadRequestException.java:
--------------------------------------------------------------------------------
1 | package com.example.polls.exception;
2 |
3 | import org.springframework.http.HttpStatus;
4 | import org.springframework.web.bind.annotation.ResponseStatus;
5 |
6 | @ResponseStatus(HttpStatus.BAD_REQUEST)
7 | public class BadRequestException extends RuntimeException {
8 |
9 | public BadRequestException(String message) {
10 | super(message);
11 | }
12 |
13 | public BadRequestException(String message, Throwable cause) {
14 | super(message, cause);
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/polling-app-server/src/main/java/com/example/polls/exception/ResourceNotFoundException.java:
--------------------------------------------------------------------------------
1 | package com.example.polls.exception;
2 |
3 | import org.springframework.http.HttpStatus;
4 | import org.springframework.web.bind.annotation.ResponseStatus;
5 |
6 | @ResponseStatus(HttpStatus.NOT_FOUND)
7 | public class ResourceNotFoundException extends RuntimeException {
8 | private String resourceName;
9 | private String fieldName;
10 | private Object fieldValue;
11 |
12 | public ResourceNotFoundException( String resourceName, String fieldName, Object fieldValue) {
13 | super(String.format("%s not found with %s : '%s'", resourceName, fieldName, fieldValue));
14 | this.resourceName = resourceName;
15 | this.fieldName = fieldName;
16 | this.fieldValue = fieldValue;
17 | }
18 |
19 | public String getResourceName() {
20 | return resourceName;
21 | }
22 |
23 | public String getFieldName() {
24 | return fieldName;
25 | }
26 |
27 | public Object getFieldValue() {
28 | return fieldValue;
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/polling-app-server/src/main/java/com/example/polls/model/Choice.java:
--------------------------------------------------------------------------------
1 | package com.example.polls.model;
2 |
3 | import javax.persistence.*;
4 | import javax.validation.constraints.NotBlank;
5 | import javax.validation.constraints.Size;
6 | import java.util.Objects;
7 |
8 | @Entity
9 | @Table(name = "choices")
10 | public class Choice {
11 | @Id
12 | @GeneratedValue(strategy = GenerationType.IDENTITY)
13 | private Long id;
14 |
15 | @NotBlank
16 | @Size(max = 40)
17 | private String text;
18 |
19 | @ManyToOne(fetch = FetchType.LAZY, optional = false)
20 | @JoinColumn(name = "poll_id", nullable = false)
21 | private Poll poll;
22 |
23 | public Choice() {
24 |
25 | }
26 |
27 | public Choice(String text) {
28 | this.text = text;
29 | }
30 |
31 | public Long getId() {
32 | return id;
33 | }
34 |
35 | public void setId(Long id) {
36 | this.id = id;
37 | }
38 |
39 | public String getText() {
40 | return text;
41 | }
42 |
43 | public void setText(String text) {
44 | this.text = text;
45 | }
46 |
47 | public Poll getPoll() {
48 | return poll;
49 | }
50 |
51 | public void setPoll(Poll poll) {
52 | this.poll = poll;
53 | }
54 |
55 | @Override
56 | public boolean equals(Object o) {
57 | if (this == o) return true;
58 | if (o == null || getClass() != o.getClass()) return false;
59 | Choice choice = (Choice) o;
60 | return Objects.equals(id, choice.id);
61 | }
62 |
63 | @Override
64 | public int hashCode() {
65 | return Objects.hash(id);
66 | }
67 | }
68 |
--------------------------------------------------------------------------------
/polling-app-server/src/main/java/com/example/polls/model/ChoiceVoteCount.java:
--------------------------------------------------------------------------------
1 | package com.example.polls.model;
2 |
3 | public class ChoiceVoteCount {
4 | private Long choiceId;
5 | private Long voteCount;
6 |
7 | public ChoiceVoteCount(Long choiceId, Long voteCount) {
8 | this.choiceId = choiceId;
9 | this.voteCount = voteCount;
10 | }
11 |
12 | public Long getChoiceId() {
13 | return choiceId;
14 | }
15 |
16 | public void setChoiceId(Long choiceId) {
17 | this.choiceId = choiceId;
18 | }
19 |
20 | public Long getVoteCount() {
21 | return voteCount;
22 | }
23 |
24 | public void setVoteCount(Long voteCount) {
25 | this.voteCount = voteCount;
26 | }
27 | }
28 |
29 |
--------------------------------------------------------------------------------
/polling-app-server/src/main/java/com/example/polls/model/Poll.java:
--------------------------------------------------------------------------------
1 | package com.example.polls.model;
2 |
3 | import com.example.polls.model.audit.UserDateAudit;
4 | import org.hibernate.annotations.BatchSize;
5 | import org.hibernate.annotations.Fetch;
6 | import org.hibernate.annotations.FetchMode;
7 | import javax.persistence.*;
8 | import javax.validation.constraints.NotBlank;
9 | import javax.validation.constraints.NotNull;
10 | import javax.validation.constraints.Size;
11 | import java.time.Instant;
12 | import java.util.ArrayList;
13 | import java.util.List;
14 |
15 | @Entity
16 | @Table(name = "polls")
17 | public class Poll extends UserDateAudit {
18 | @Id
19 | @GeneratedValue(strategy = GenerationType.IDENTITY)
20 | private Long id;
21 |
22 | @NotBlank
23 | @Size(max = 140)
24 | private String question;
25 |
26 | @OneToMany(
27 | mappedBy = "poll",
28 | cascade = CascadeType.ALL,
29 | fetch = FetchType.EAGER,
30 | orphanRemoval = true
31 | )
32 | @Size(min = 2, max = 6)
33 | @Fetch(FetchMode.SELECT)
34 | @BatchSize(size = 30)
35 | private List choices = new ArrayList<>();
36 |
37 | @NotNull
38 | private Instant expirationDateTime;
39 |
40 | public Long getId() {
41 | return id;
42 | }
43 |
44 | public void setId(Long id) {
45 | this.id = id;
46 | }
47 |
48 | public String getQuestion() {
49 | return question;
50 | }
51 |
52 | public void setQuestion(String question) {
53 | this.question = question;
54 | }
55 |
56 | public List getChoices() {
57 | return choices;
58 | }
59 |
60 | public void setChoices(List choices) {
61 | this.choices = choices;
62 | }
63 |
64 | public Instant getExpirationDateTime() {
65 | return expirationDateTime;
66 | }
67 |
68 | public void setExpirationDateTime(Instant expirationDateTime) {
69 | this.expirationDateTime = expirationDateTime;
70 | }
71 |
72 | public void addChoice(Choice choice) {
73 | choices.add(choice);
74 | choice.setPoll(this);
75 | }
76 |
77 | public void removeChoice(Choice choice) {
78 | choices.remove(choice);
79 | choice.setPoll(null);
80 | }
81 | }
82 |
--------------------------------------------------------------------------------
/polling-app-server/src/main/java/com/example/polls/model/Role.java:
--------------------------------------------------------------------------------
1 | package com.example.polls.model;
2 |
3 | import org.hibernate.annotations.NaturalId;
4 | import javax.persistence.*;
5 |
6 | @Entity
7 | @Table(name = "roles")
8 | public class Role {
9 | @Id
10 | @GeneratedValue(strategy = GenerationType.IDENTITY)
11 | private Long id;
12 |
13 | @Enumerated(EnumType.STRING)
14 | @NaturalId
15 | @Column(length = 60)
16 | private RoleName name;
17 |
18 | public Role() {
19 |
20 | }
21 |
22 | public Role(RoleName name) {
23 | this.name = name;
24 | }
25 |
26 | public Long getId() {
27 | return id;
28 | }
29 |
30 | public void setId(Long id) {
31 | this.id = id;
32 | }
33 |
34 | public RoleName getName() {
35 | return name;
36 | }
37 |
38 | public void setName(RoleName name) {
39 | this.name = name;
40 | }
41 |
42 | }
43 |
--------------------------------------------------------------------------------
/polling-app-server/src/main/java/com/example/polls/model/RoleName.java:
--------------------------------------------------------------------------------
1 | package com.example.polls.model;
2 |
3 | public enum RoleName {
4 | ROLE_USER,
5 | ROLE_ADMIN
6 | }
7 |
--------------------------------------------------------------------------------
/polling-app-server/src/main/java/com/example/polls/model/User.java:
--------------------------------------------------------------------------------
1 | package com.example.polls.model;
2 |
3 | import com.example.polls.model.audit.DateAudit;
4 | import org.hibernate.annotations.NaturalId;
5 | import javax.persistence.*;
6 | import javax.validation.constraints.Email;
7 | import javax.validation.constraints.NotBlank;
8 | import javax.validation.constraints.Size;
9 | import java.util.HashSet;
10 | import java.util.Set;
11 |
12 | @Entity
13 | @Table(name = "users", uniqueConstraints = {
14 | @UniqueConstraint(columnNames = {
15 | "username"
16 | }),
17 | @UniqueConstraint(columnNames = {
18 | "email"
19 | })
20 | })
21 | public class User extends DateAudit {
22 | @Id
23 | @GeneratedValue(strategy = GenerationType.IDENTITY)
24 | private Long id;
25 |
26 | @NotBlank
27 | @Size(max = 40)
28 | private String name;
29 |
30 | @NotBlank
31 | @Size(max = 15)
32 | private String username;
33 |
34 | @NaturalId
35 | @NotBlank
36 | @Size(max = 40)
37 | @Email
38 | private String email;
39 |
40 | @NotBlank
41 | @Size(max = 100)
42 | private String password;
43 |
44 | @ManyToMany(fetch = FetchType.LAZY)
45 | @JoinTable(name = "user_roles",
46 | joinColumns = @JoinColumn(name = "user_id"),
47 | inverseJoinColumns = @JoinColumn(name = "role_id"))
48 | private Set roles = new HashSet<>();
49 |
50 | public User() {
51 |
52 | }
53 |
54 | public User(String name, String username, String email, String password) {
55 | this.name = name;
56 | this.username = username;
57 | this.email = email;
58 | this.password = password;
59 | }
60 |
61 | public Long getId() {
62 | return id;
63 | }
64 |
65 | public void setId(Long id) {
66 | this.id = id;
67 | }
68 |
69 | public String getUsername() {
70 | return username;
71 | }
72 |
73 | public void setUsername(String username) {
74 | this.username = username;
75 | }
76 |
77 | public String getName() {
78 | return name;
79 | }
80 |
81 | public void setName(String name) {
82 | this.name = name;
83 | }
84 |
85 | public String getEmail() {
86 | return email;
87 | }
88 |
89 | public void setEmail(String email) {
90 | this.email = email;
91 | }
92 |
93 | public String getPassword() {
94 | return password;
95 | }
96 |
97 | public void setPassword(String password) {
98 | this.password = password;
99 | }
100 |
101 | public Set getRoles() {
102 | return roles;
103 | }
104 |
105 | public void setRoles(Set roles) {
106 | this.roles = roles;
107 | }
108 | }
--------------------------------------------------------------------------------
/polling-app-server/src/main/java/com/example/polls/model/Vote.java:
--------------------------------------------------------------------------------
1 | package com.example.polls.model;
2 |
3 | import com.example.polls.model.audit.DateAudit;
4 | import javax.persistence.*;
5 |
6 | @Entity
7 | @Table(name = "votes", uniqueConstraints = {
8 | @UniqueConstraint(columnNames = {
9 | "poll_id",
10 | "user_id"
11 | })
12 | })
13 | public class Vote extends DateAudit {
14 | @Id
15 | @GeneratedValue(strategy = GenerationType.IDENTITY)
16 | private Long id;
17 |
18 | @ManyToOne(fetch = FetchType.LAZY, optional = false)
19 | @JoinColumn(name = "poll_id", nullable = false)
20 | private Poll poll;
21 |
22 | @ManyToOne(fetch = FetchType.LAZY, optional = false)
23 | @JoinColumn(name = "choice_id", nullable = false)
24 | private Choice choice;
25 |
26 | @ManyToOne(fetch = FetchType.LAZY, optional = false)
27 | @JoinColumn(name = "user_id", nullable = false)
28 | private User user;
29 |
30 | public Long getId() {
31 | return id;
32 | }
33 |
34 | public void setId(Long id) {
35 | this.id = id;
36 | }
37 |
38 | public Poll getPoll() {
39 | return poll;
40 | }
41 |
42 | public void setPoll(Poll poll) {
43 | this.poll = poll;
44 | }
45 |
46 | public Choice getChoice() {
47 | return choice;
48 | }
49 |
50 | public void setChoice(Choice choice) {
51 | this.choice = choice;
52 | }
53 |
54 | public User getUser() {
55 | return user;
56 | }
57 |
58 | public void setUser(User user) {
59 | this.user = user;
60 | }
61 | }
62 |
--------------------------------------------------------------------------------
/polling-app-server/src/main/java/com/example/polls/model/audit/DateAudit.java:
--------------------------------------------------------------------------------
1 | package com.example.polls.model.audit;
2 |
3 | import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
4 | import org.springframework.data.annotation.CreatedDate;
5 | import org.springframework.data.annotation.LastModifiedDate;
6 | import org.springframework.data.jpa.domain.support.AuditingEntityListener;
7 |
8 | import javax.persistence.EntityListeners;
9 | import javax.persistence.MappedSuperclass;
10 | import java.io.Serializable;
11 | import java.time.Instant;
12 |
13 | @MappedSuperclass
14 | @EntityListeners(AuditingEntityListener.class)
15 | @JsonIgnoreProperties(
16 | value = {"createdAt", "updatedAt"},
17 | allowGetters = true
18 | )
19 | public abstract class DateAudit implements Serializable {
20 |
21 | @CreatedDate
22 | private Instant createdAt;
23 |
24 | @LastModifiedDate
25 | private Instant updatedAt;
26 |
27 | public Instant getCreatedAt() {
28 | return createdAt;
29 | }
30 |
31 | public void setCreatedAt(Instant createdAt) {
32 | this.createdAt = createdAt;
33 | }
34 |
35 | public Instant getUpdatedAt() {
36 | return updatedAt;
37 | }
38 |
39 | public void setUpdatedAt(Instant updatedAt) {
40 | this.updatedAt = updatedAt;
41 | }
42 |
43 | }
44 |
--------------------------------------------------------------------------------
/polling-app-server/src/main/java/com/example/polls/model/audit/UserDateAudit.java:
--------------------------------------------------------------------------------
1 | package com.example.polls.model.audit;
2 |
3 | import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
4 | import org.springframework.data.annotation.CreatedBy;
5 | import org.springframework.data.annotation.LastModifiedBy;
6 |
7 | import javax.persistence.MappedSuperclass;
8 |
9 |
10 |
11 | @MappedSuperclass
12 | @JsonIgnoreProperties(
13 | value = {"createdBy", "updatedBy"},
14 | allowGetters = true
15 | )
16 | public abstract class UserDateAudit extends DateAudit {
17 |
18 | @CreatedBy
19 | private Long createdBy;
20 |
21 | @LastModifiedBy
22 | private Long updatedBy;
23 |
24 | public Long getCreatedBy() {
25 | return createdBy;
26 | }
27 |
28 | public void setCreatedBy(Long createdBy) {
29 | this.createdBy = createdBy;
30 | }
31 |
32 | public Long getUpdatedBy() {
33 | return updatedBy;
34 | }
35 |
36 | public void setUpdatedBy(Long updatedBy) {
37 | this.updatedBy = updatedBy;
38 | }
39 | }
40 |
--------------------------------------------------------------------------------
/polling-app-server/src/main/java/com/example/polls/payload/ApiResponse.java:
--------------------------------------------------------------------------------
1 | package com.example.polls.payload;
2 |
3 | public class ApiResponse {
4 | private Boolean success;
5 | private String message;
6 |
7 | public ApiResponse(Boolean success, String message) {
8 | this.success = success;
9 | this.message = message;
10 | }
11 |
12 | public Boolean getSuccess() {
13 | return success;
14 | }
15 |
16 | public void setSuccess(Boolean success) {
17 | this.success = success;
18 | }
19 |
20 | public String getMessage() {
21 | return message;
22 | }
23 |
24 | public void setMessage(String message) {
25 | this.message = message;
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/polling-app-server/src/main/java/com/example/polls/payload/ChoiceRequest.java:
--------------------------------------------------------------------------------
1 | package com.example.polls.payload;
2 |
3 | import javax.validation.constraints.NotBlank;
4 | import javax.validation.constraints.Size;
5 |
6 | public class ChoiceRequest {
7 | @NotBlank
8 | @Size(max = 40)
9 | private String text;
10 |
11 | public String getText() {
12 | return text;
13 | }
14 |
15 | public void setText(String text) {
16 | this.text = text;
17 | }
18 | }
19 |
--------------------------------------------------------------------------------
/polling-app-server/src/main/java/com/example/polls/payload/ChoiceResponse.java:
--------------------------------------------------------------------------------
1 | package com.example.polls.payload;
2 |
3 | public class ChoiceResponse {
4 | private long id;
5 | private String text;
6 | private long voteCount;
7 |
8 | public long getId() {
9 | return id;
10 | }
11 |
12 | public void setId(long id) {
13 | this.id = id;
14 | }
15 |
16 | public String getText() {
17 | return text;
18 | }
19 |
20 | public void setText(String text) {
21 | this.text = text;
22 | }
23 |
24 | public long getVoteCount() {
25 | return voteCount;
26 | }
27 |
28 | public void setVoteCount(long voteCount) {
29 | this.voteCount = voteCount;
30 | }
31 |
32 | }
33 |
--------------------------------------------------------------------------------
/polling-app-server/src/main/java/com/example/polls/payload/JwtAuthenticationResponse.java:
--------------------------------------------------------------------------------
1 | package com.example.polls.payload;
2 |
3 | public class JwtAuthenticationResponse {
4 | private String accessToken;
5 | private String tokenType = "Bearer";
6 |
7 | public JwtAuthenticationResponse(String accessToken) {
8 | this.accessToken = accessToken;
9 | }
10 |
11 | public String getAccessToken() {
12 | return accessToken;
13 | }
14 |
15 | public void setAccessToken(String accessToken) {
16 | this.accessToken = accessToken;
17 | }
18 |
19 | public String getTokenType() {
20 | return tokenType;
21 | }
22 |
23 | public void setTokenType(String tokenType) {
24 | this.tokenType = tokenType;
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/polling-app-server/src/main/java/com/example/polls/payload/LoginRequest.java:
--------------------------------------------------------------------------------
1 | package com.example.polls.payload;
2 |
3 | import javax.validation.constraints.NotBlank;
4 |
5 | public class LoginRequest {
6 | @NotBlank
7 | private String usernameOrEmail;
8 |
9 | @NotBlank
10 | private String password;
11 |
12 | public String getUsernameOrEmail() {
13 | return usernameOrEmail;
14 | }
15 |
16 | public void setUsernameOrEmail(String usernameOrEmail) {
17 | this.usernameOrEmail = usernameOrEmail;
18 | }
19 |
20 | public String getPassword() {
21 | return password;
22 | }
23 |
24 | public void setPassword(String password) {
25 | this.password = password;
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/polling-app-server/src/main/java/com/example/polls/payload/PagedResponse.java:
--------------------------------------------------------------------------------
1 | package com.example.polls.payload;
2 |
3 | import java.util.List;
4 |
5 | public class PagedResponse {
6 |
7 | private List content;
8 | private int page;
9 | private int size;
10 | private long totalElements;
11 | private int totalPages;
12 | private boolean last;
13 |
14 | public PagedResponse() {
15 |
16 | }
17 |
18 | public PagedResponse(List content, int page, int size, long totalElements, int totalPages, boolean last) {
19 | this.content = content;
20 | this.page = page;
21 | this.size = size;
22 | this.totalElements = totalElements;
23 | this.totalPages = totalPages;
24 | this.last = last;
25 | }
26 |
27 | public List getContent() {
28 | return content;
29 | }
30 |
31 | public void setContent(List content) {
32 | this.content = content;
33 | }
34 |
35 | public int getPage() {
36 | return page;
37 | }
38 |
39 | public void setPage(int page) {
40 | this.page = page;
41 | }
42 |
43 | public int getSize() {
44 | return size;
45 | }
46 |
47 | public void setSize(int size) {
48 | this.size = size;
49 | }
50 |
51 | public long getTotalElements() {
52 | return totalElements;
53 | }
54 |
55 | public void setTotalElements(long totalElements) {
56 | this.totalElements = totalElements;
57 | }
58 |
59 | public int getTotalPages() {
60 | return totalPages;
61 | }
62 |
63 | public void setTotalPages(int totalPages) {
64 | this.totalPages = totalPages;
65 | }
66 |
67 | public boolean isLast() {
68 | return last;
69 | }
70 |
71 | public void setLast(boolean last) {
72 | this.last = last;
73 | }
74 | }
75 |
--------------------------------------------------------------------------------
/polling-app-server/src/main/java/com/example/polls/payload/PollLength.java:
--------------------------------------------------------------------------------
1 | package com.example.polls.payload;
2 |
3 | import javax.validation.constraints.Max;
4 | import javax.validation.constraints.NotNull;
5 |
6 | public class PollLength {
7 | @NotNull
8 | @Max(7)
9 | private Integer days;
10 |
11 | @NotNull
12 | @Max(23)
13 | private Integer hours;
14 |
15 | public int getDays() {
16 | return days;
17 | }
18 |
19 | public void setDays(int days) {
20 | this.days = days;
21 | }
22 |
23 | public int getHours() {
24 | return hours;
25 | }
26 |
27 | public void setHours(int hours) {
28 | this.hours = hours;
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/polling-app-server/src/main/java/com/example/polls/payload/PollRequest.java:
--------------------------------------------------------------------------------
1 | package com.example.polls.payload;
2 |
3 | import javax.validation.Valid;
4 | import javax.validation.constraints.NotBlank;
5 | import javax.validation.constraints.NotNull;
6 | import javax.validation.constraints.Size;
7 | import java.util.List;
8 |
9 | public class PollRequest {
10 | @NotBlank
11 | @Size(max = 140)
12 | private String question;
13 |
14 | @NotNull
15 | @Size(min = 2, max = 6)
16 | @Valid
17 | private List choices;
18 |
19 | @NotNull
20 | @Valid
21 | private PollLength pollLength;
22 |
23 | public String getQuestion() {
24 | return question;
25 | }
26 |
27 | public void setQuestion(String question) {
28 | this.question = question;
29 | }
30 |
31 | public List getChoices() {
32 | return choices;
33 | }
34 |
35 | public void setChoices(List choices) {
36 | this.choices = choices;
37 | }
38 |
39 | public PollLength getPollLength() {
40 | return pollLength;
41 | }
42 |
43 | public void setPollLength(PollLength pollLength) {
44 | this.pollLength = pollLength;
45 | }
46 | }
47 |
--------------------------------------------------------------------------------
/polling-app-server/src/main/java/com/example/polls/payload/PollResponse.java:
--------------------------------------------------------------------------------
1 | package com.example.polls.payload;
2 |
3 | import com.fasterxml.jackson.annotation.JsonInclude;
4 |
5 | import java.time.Instant;
6 | import java.util.List;
7 |
8 | public class PollResponse {
9 | private Long id;
10 | private String question;
11 | private List choices;
12 | private UserSummary createdBy;
13 | private Instant creationDateTime;
14 | private Instant expirationDateTime;
15 | private Boolean isExpired;
16 |
17 | @JsonInclude(JsonInclude.Include.NON_NULL)
18 | private Long selectedChoice;
19 | private Long totalVotes;
20 |
21 | public Long getId() {
22 | return id;
23 | }
24 |
25 | public void setId(Long id) {
26 | this.id = id;
27 | }
28 |
29 | public String getQuestion() {
30 | return question;
31 | }
32 |
33 | public void setQuestion(String question) {
34 | this.question = question;
35 | }
36 |
37 | public List getChoices() {
38 | return choices;
39 | }
40 |
41 | public void setChoices(List choices) {
42 | this.choices = choices;
43 | }
44 |
45 | public UserSummary getCreatedBy() {
46 | return createdBy;
47 | }
48 |
49 | public void setCreatedBy(UserSummary createdBy) {
50 | this.createdBy = createdBy;
51 | }
52 |
53 |
54 | public Instant getCreationDateTime() {
55 | return creationDateTime;
56 | }
57 |
58 | public void setCreationDateTime(Instant creationDateTime) {
59 | this.creationDateTime = creationDateTime;
60 | }
61 |
62 | public Instant getExpirationDateTime() {
63 | return expirationDateTime;
64 | }
65 |
66 | public void setExpirationDateTime(Instant expirationDateTime) {
67 | this.expirationDateTime = expirationDateTime;
68 | }
69 |
70 | public Boolean getExpired() {
71 | return isExpired;
72 | }
73 |
74 | public void setExpired(Boolean expired) {
75 | isExpired = expired;
76 | }
77 |
78 | public Long getSelectedChoice() {
79 | return selectedChoice;
80 | }
81 |
82 | public void setSelectedChoice(Long selectedChoice) {
83 | this.selectedChoice = selectedChoice;
84 | }
85 |
86 | public Long getTotalVotes() {
87 | return totalVotes;
88 | }
89 |
90 | public void setTotalVotes(Long totalVotes) {
91 | this.totalVotes = totalVotes;
92 | }
93 | }
94 |
--------------------------------------------------------------------------------
/polling-app-server/src/main/java/com/example/polls/payload/SignUpRequest.java:
--------------------------------------------------------------------------------
1 | package com.example.polls.payload;
2 |
3 | import javax.validation.constraints.*;
4 |
5 | public class SignUpRequest {
6 | @NotBlank
7 | @Size(min = 4, max = 40)
8 | private String name;
9 |
10 | @NotBlank
11 | @Size(min = 3, max = 15)
12 | private String username;
13 |
14 | @NotBlank
15 | @Size(max = 40)
16 | @Email
17 | private String email;
18 |
19 | @NotBlank
20 | @Size(min = 6, max = 20)
21 | private String password;
22 |
23 | public String getName() {
24 | return name;
25 | }
26 |
27 | public void setName(String name) {
28 | this.name = name;
29 | }
30 |
31 | public String getUsername() {
32 | return username;
33 | }
34 |
35 | public void setUsername(String username) {
36 | this.username = username;
37 | }
38 |
39 | public String getEmail() {
40 | return email;
41 | }
42 |
43 | public void setEmail(String email) {
44 | this.email = email;
45 | }
46 |
47 | public String getPassword() {
48 | return password;
49 | }
50 |
51 | public void setPassword(String password) {
52 | this.password = password;
53 | }
54 | }
55 |
--------------------------------------------------------------------------------
/polling-app-server/src/main/java/com/example/polls/payload/UserIdentityAvailability.java:
--------------------------------------------------------------------------------
1 | package com.example.polls.payload;
2 |
3 | public class UserIdentityAvailability {
4 | private Boolean available;
5 |
6 | public UserIdentityAvailability(Boolean available) {
7 | this.available = available;
8 | }
9 |
10 | public Boolean getAvailable() {
11 | return available;
12 | }
13 |
14 | public void setAvailable(Boolean available) {
15 | this.available = available;
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/polling-app-server/src/main/java/com/example/polls/payload/UserProfile.java:
--------------------------------------------------------------------------------
1 | package com.example.polls.payload;
2 |
3 | import java.time.Instant;
4 |
5 | public class UserProfile {
6 | private Long id;
7 | private String username;
8 | private String name;
9 | private Instant joinedAt;
10 | private Long pollCount;
11 | private Long voteCount;
12 |
13 | public UserProfile(Long id, String username, String name, Instant joinedAt, Long pollCount, Long voteCount) {
14 | this.id = id;
15 | this.username = username;
16 | this.name = name;
17 | this.joinedAt = joinedAt;
18 | this.pollCount = pollCount;
19 | this.voteCount = voteCount;
20 | }
21 |
22 | public Long getId() {
23 | return id;
24 | }
25 |
26 | public void setId(Long id) {
27 | this.id = id;
28 | }
29 |
30 | public String getUsername() {
31 | return username;
32 | }
33 |
34 | public void setUsername(String username) {
35 | this.username = username;
36 | }
37 |
38 | public String getName() {
39 | return name;
40 | }
41 |
42 | public void setName(String name) {
43 | this.name = name;
44 | }
45 |
46 | public Instant getJoinedAt() {
47 | return joinedAt;
48 | }
49 |
50 | public void setJoinedAt(Instant joinedAt) {
51 | this.joinedAt = joinedAt;
52 | }
53 |
54 | public Long getPollCount() {
55 | return pollCount;
56 | }
57 |
58 | public void setPollCount(Long pollCount) {
59 | this.pollCount = pollCount;
60 | }
61 |
62 | public Long getVoteCount() {
63 | return voteCount;
64 | }
65 |
66 | public void setVoteCount(Long voteCount) {
67 | this.voteCount = voteCount;
68 | }
69 | }
70 |
--------------------------------------------------------------------------------
/polling-app-server/src/main/java/com/example/polls/payload/UserSummary.java:
--------------------------------------------------------------------------------
1 | package com.example.polls.payload;
2 |
3 | public class UserSummary {
4 | private Long id;
5 | private String username;
6 | private String name;
7 |
8 | public UserSummary(Long id, String username, String name) {
9 | this.id = id;
10 | this.username = username;
11 | this.name = name;
12 | }
13 |
14 | public Long getId() {
15 | return id;
16 | }
17 |
18 | public void setId(Long id) {
19 | this.id = id;
20 | }
21 |
22 | public String getUsername() {
23 | return username;
24 | }
25 |
26 | public void setUsername(String username) {
27 | this.username = username;
28 | }
29 |
30 | public String getName() {
31 | return name;
32 | }
33 |
34 | public void setName(String name) {
35 | this.name = name;
36 | }
37 | }
38 |
--------------------------------------------------------------------------------
/polling-app-server/src/main/java/com/example/polls/payload/VoteRequest.java:
--------------------------------------------------------------------------------
1 | package com.example.polls.payload;
2 | import javax.validation.constraints.NotNull;
3 |
4 | public class VoteRequest {
5 | @NotNull
6 | private Long choiceId;
7 |
8 | public Long getChoiceId() {
9 | return choiceId;
10 | }
11 |
12 | public void setChoiceId(Long choiceId) {
13 | this.choiceId = choiceId;
14 | }
15 | }
16 |
17 |
--------------------------------------------------------------------------------
/polling-app-server/src/main/java/com/example/polls/repository/PollRepository.java:
--------------------------------------------------------------------------------
1 | package com.example.polls.repository;
2 |
3 | import com.example.polls.model.Poll;
4 | import org.springframework.data.domain.Page;
5 | import org.springframework.data.domain.Pageable;
6 | import org.springframework.data.domain.Sort;
7 | import org.springframework.data.jpa.repository.JpaRepository;
8 | import org.springframework.stereotype.Repository;
9 |
10 | import java.util.List;
11 | import java.util.Optional;
12 |
13 | @Repository
14 | public interface PollRepository extends JpaRepository {
15 |
16 | Optional findById(Long pollId);
17 |
18 | Page findByCreatedBy(Long userId, Pageable pageable);
19 |
20 | long countByCreatedBy(Long userId);
21 |
22 | List findByIdIn(List pollIds);
23 |
24 | List findByIdIn(List pollIds, Sort sort);
25 | }
26 |
--------------------------------------------------------------------------------
/polling-app-server/src/main/java/com/example/polls/repository/RoleRepository.java:
--------------------------------------------------------------------------------
1 | package com.example.polls.repository;
2 |
3 | import com.example.polls.model.Role;
4 | import com.example.polls.model.RoleName;
5 | import org.springframework.data.jpa.repository.JpaRepository;
6 | import org.springframework.stereotype.Repository;
7 |
8 | import java.util.Optional;
9 |
10 | @Repository
11 | public interface RoleRepository extends JpaRepository {
12 | Optional findByName(RoleName roleName);
13 | }
14 |
--------------------------------------------------------------------------------
/polling-app-server/src/main/java/com/example/polls/repository/UserRepository.java:
--------------------------------------------------------------------------------
1 | package com.example.polls.repository;
2 |
3 | import com.example.polls.model.User;
4 | import org.springframework.data.jpa.repository.JpaRepository;
5 | import org.springframework.stereotype.Repository;
6 |
7 | import java.util.List;
8 | import java.util.Optional;
9 |
10 | @Repository
11 | public interface UserRepository extends JpaRepository {
12 | Optional findByEmail(String email);
13 |
14 | Optional findByUsernameOrEmail(String username, String email);
15 |
16 | List findByIdIn(List userIds);
17 |
18 | Optional findByUsername(String username);
19 |
20 | Boolean existsByUsername(String username);
21 |
22 | Boolean existsByEmail(String email);
23 | }
24 |
--------------------------------------------------------------------------------
/polling-app-server/src/main/java/com/example/polls/repository/VoteRepository.java:
--------------------------------------------------------------------------------
1 | package com.example.polls.repository;
2 |
3 | import com.example.polls.model.ChoiceVoteCount;
4 | import com.example.polls.model.Vote;
5 | import org.springframework.data.domain.Page;
6 | import org.springframework.data.domain.Pageable;
7 | import org.springframework.data.jpa.repository.JpaRepository;
8 | import org.springframework.data.jpa.repository.Query;
9 | import org.springframework.data.repository.query.Param;
10 | import org.springframework.stereotype.Repository;
11 |
12 | import java.util.List;
13 |
14 | @Repository
15 | public interface VoteRepository extends JpaRepository {
16 | @Query("SELECT NEW com.example.polls.model.ChoiceVoteCount(v.choice.id, count(v.id)) FROM Vote v WHERE v.poll.id in :pollIds GROUP BY v.choice.id")
17 | List countByPollIdInGroupByChoiceId(@Param("pollIds") List pollIds);
18 |
19 | @Query("SELECT NEW com.example.polls.model.ChoiceVoteCount(v.choice.id, count(v.id)) FROM Vote v WHERE v.poll.id = :pollId GROUP BY v.choice.id")
20 | List countByPollIdGroupByChoiceId(@Param("pollId") Long pollId);
21 |
22 | @Query("SELECT v FROM Vote v where v.user.id = :userId and v.poll.id in :pollIds")
23 | List findByUserIdAndPollIdIn(@Param("userId") Long userId, @Param("pollIds") List pollIds);
24 |
25 | @Query("SELECT v FROM Vote v where v.user.id = :userId and v.poll.id = :pollId")
26 | Vote findByUserIdAndPollId(@Param("userId") Long userId, @Param("pollId") Long pollId);
27 |
28 | @Query("SELECT COUNT(v.id) from Vote v where v.user.id = :userId")
29 | long countByUserId(@Param("userId") Long userId);
30 |
31 | @Query("SELECT v.poll.id FROM Vote v WHERE v.user.id = :userId")
32 | Page findVotedPollIdsByUserId(@Param("userId") Long userId, Pageable pageable);
33 | }
34 |
35 |
--------------------------------------------------------------------------------
/polling-app-server/src/main/java/com/example/polls/security/CurrentUser.java:
--------------------------------------------------------------------------------
1 | package com.example.polls.security;
2 |
3 | import org.springframework.security.core.annotation.AuthenticationPrincipal;
4 |
5 | import java.lang.annotation.*;
6 |
7 | @Target({ElementType.PARAMETER, ElementType.TYPE})
8 | @Retention(RetentionPolicy.RUNTIME)
9 | @Documented
10 | @AuthenticationPrincipal
11 | public @interface CurrentUser {
12 |
13 | }
14 |
--------------------------------------------------------------------------------
/polling-app-server/src/main/java/com/example/polls/security/CustomUserDetailsService.java:
--------------------------------------------------------------------------------
1 | package com.example.polls.security;
2 |
3 | import com.example.polls.exception.ResourceNotFoundException;
4 | import com.example.polls.model.User;
5 | import com.example.polls.repository.UserRepository;
6 | import org.springframework.beans.factory.annotation.Autowired;
7 | import org.springframework.security.core.userdetails.UserDetails;
8 | import org.springframework.security.core.userdetails.UserDetailsService;
9 | import org.springframework.security.core.userdetails.UsernameNotFoundException;
10 | import org.springframework.stereotype.Service;
11 | import org.springframework.transaction.annotation.Transactional;
12 |
13 | @Service
14 | public class CustomUserDetailsService implements UserDetailsService {
15 |
16 | @Autowired
17 | UserRepository userRepository;
18 |
19 | @Override
20 | @Transactional
21 | public UserDetails loadUserByUsername(String usernameOrEmail)
22 | throws UsernameNotFoundException {
23 | // Let people login with either username or email
24 | User user = userRepository.findByUsernameOrEmail(usernameOrEmail, usernameOrEmail)
25 | .orElseThrow(() ->
26 | new UsernameNotFoundException("User not found with username or email : " + usernameOrEmail)
27 | );
28 |
29 | return UserPrincipal.create(user);
30 | }
31 |
32 | @Transactional
33 | public UserDetails loadUserById(Long id) {
34 | User user = userRepository.findById(id).orElseThrow(
35 | () -> new ResourceNotFoundException("User", "id", id)
36 | );
37 |
38 | return UserPrincipal.create(user);
39 | }
40 | }
--------------------------------------------------------------------------------
/polling-app-server/src/main/java/com/example/polls/security/JwtAuthenticationEntryPoint.java:
--------------------------------------------------------------------------------
1 | package com.example.polls.security;
2 |
3 | import org.slf4j.Logger;
4 | import org.slf4j.LoggerFactory;
5 | import org.springframework.security.core.AuthenticationException;
6 | import org.springframework.security.web.AuthenticationEntryPoint;
7 | import org.springframework.stereotype.Component;
8 |
9 | import javax.servlet.ServletException;
10 | import javax.servlet.http.HttpServletRequest;
11 | import javax.servlet.http.HttpServletResponse;
12 | import java.io.IOException;
13 |
14 | @Component
15 | public class JwtAuthenticationEntryPoint implements AuthenticationEntryPoint {
16 |
17 | private static final Logger logger = LoggerFactory.getLogger(JwtAuthenticationEntryPoint.class);
18 | @Override
19 | public void commence(HttpServletRequest httpServletRequest,
20 | HttpServletResponse httpServletResponse,
21 | AuthenticationException e) throws IOException, ServletException {
22 | logger.error("Responding with unauthorized error. Message - {}", e.getMessage());
23 | httpServletResponse.sendError(HttpServletResponse.SC_UNAUTHORIZED, e.getMessage());
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/polling-app-server/src/main/java/com/example/polls/security/JwtAuthenticationFilter.java:
--------------------------------------------------------------------------------
1 | package com.example.polls.security;
2 |
3 | import org.slf4j.Logger;
4 | import org.slf4j.LoggerFactory;
5 | import org.springframework.beans.factory.annotation.Autowired;
6 | import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
7 | import org.springframework.security.core.context.SecurityContextHolder;
8 | import org.springframework.security.core.userdetails.UserDetails;
9 | import org.springframework.security.web.authentication.WebAuthenticationDetailsSource;
10 | import org.springframework.util.StringUtils;
11 | import org.springframework.web.filter.OncePerRequestFilter;
12 |
13 | import javax.servlet.FilterChain;
14 | import javax.servlet.ServletException;
15 | import javax.servlet.http.HttpServletRequest;
16 | import javax.servlet.http.HttpServletResponse;
17 | import java.io.IOException;
18 |
19 | public class JwtAuthenticationFilter extends OncePerRequestFilter {
20 |
21 | @Autowired
22 | private JwtTokenProvider tokenProvider;
23 |
24 | @Autowired
25 | private CustomUserDetailsService customUserDetailsService;
26 |
27 | private static final Logger logger = LoggerFactory.getLogger(JwtAuthenticationFilter.class);
28 |
29 | @Override
30 | protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
31 | try {
32 | String jwt = getJwtFromRequest(request);
33 |
34 | if (StringUtils.hasText(jwt) && tokenProvider.validateToken(jwt)) {
35 | Long userId = tokenProvider.getUserIdFromJWT(jwt);
36 |
37 | /*
38 | Note that you could also encode the user's username and roles inside JWT claims
39 | and create the UserDetails object by parsing those claims from the JWT.
40 | That would avoid the following database hit. It's completely up to you.
41 | */
42 | UserDetails userDetails = customUserDetailsService.loadUserById(userId);
43 | UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities());
44 | authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
45 |
46 | SecurityContextHolder.getContext().setAuthentication(authentication);
47 | }
48 | } catch (Exception ex) {
49 | logger.error("Could not set user authentication in security context", ex);
50 | }
51 |
52 | filterChain.doFilter(request, response);
53 | }
54 |
55 | private String getJwtFromRequest(HttpServletRequest request) {
56 | String bearerToken = request.getHeader("Authorization");
57 | if (StringUtils.hasText(bearerToken) && bearerToken.startsWith("Bearer ")) {
58 | return bearerToken.substring(7, bearerToken.length());
59 | }
60 | return null;
61 | }
62 | }
63 |
--------------------------------------------------------------------------------
/polling-app-server/src/main/java/com/example/polls/security/JwtTokenProvider.java:
--------------------------------------------------------------------------------
1 | package com.example.polls.security;
2 |
3 | import io.jsonwebtoken.*;
4 | import org.slf4j.Logger;
5 | import org.slf4j.LoggerFactory;
6 | import org.springframework.beans.factory.annotation.Value;
7 | import org.springframework.security.core.Authentication;
8 | import org.springframework.stereotype.Component;
9 |
10 | import java.util.Date;
11 |
12 | @Component
13 | public class JwtTokenProvider {
14 |
15 | private static final Logger logger = LoggerFactory.getLogger(JwtTokenProvider.class);
16 |
17 | @Value("${app.jwtSecret}")
18 | private String jwtSecret;
19 |
20 | @Value("${app.jwtExpirationInMs}")
21 | private int jwtExpirationInMs;
22 |
23 | public String generateToken(Authentication authentication) {
24 |
25 | UserPrincipal userPrincipal = (UserPrincipal) authentication.getPrincipal();
26 |
27 | Date now = new Date();
28 | Date expiryDate = new Date(now.getTime() + jwtExpirationInMs);
29 |
30 | return Jwts.builder()
31 | .setSubject(Long.toString(userPrincipal.getId()))
32 | .setIssuedAt(new Date())
33 | .setExpiration(expiryDate)
34 | .signWith(SignatureAlgorithm.HS512, jwtSecret)
35 | .compact();
36 | }
37 |
38 | public Long getUserIdFromJWT(String token) {
39 | Claims claims = Jwts.parser()
40 | .setSigningKey(jwtSecret)
41 | .parseClaimsJws(token)
42 | .getBody();
43 |
44 | return Long.parseLong(claims.getSubject());
45 | }
46 |
47 | public boolean validateToken(String authToken) {
48 | try {
49 | Jwts.parser().setSigningKey(jwtSecret).parseClaimsJws(authToken);
50 | return true;
51 | } catch (SignatureException ex) {
52 | logger.error("Invalid JWT signature");
53 | } catch (MalformedJwtException ex) {
54 | logger.error("Invalid JWT token");
55 | } catch (ExpiredJwtException ex) {
56 | logger.error("Expired JWT token");
57 | } catch (UnsupportedJwtException ex) {
58 | logger.error("Unsupported JWT token");
59 | } catch (IllegalArgumentException ex) {
60 | logger.error("JWT claims string is empty.");
61 | }
62 | return false;
63 | }
64 | }
65 |
--------------------------------------------------------------------------------
/polling-app-server/src/main/java/com/example/polls/security/UserPrincipal.java:
--------------------------------------------------------------------------------
1 | package com.example.polls.security;
2 |
3 | import com.example.polls.model.User;
4 | import com.fasterxml.jackson.annotation.JsonIgnore;
5 | import org.springframework.security.core.GrantedAuthority;
6 | import org.springframework.security.core.authority.SimpleGrantedAuthority;
7 | import org.springframework.security.core.userdetails.UserDetails;
8 |
9 | import java.util.Collection;
10 | import java.util.List;
11 | import java.util.Objects;
12 | import java.util.stream.Collectors;
13 |
14 | public class UserPrincipal implements UserDetails {
15 | private Long id;
16 |
17 | private String name;
18 |
19 | private String username;
20 |
21 | @JsonIgnore
22 | private String email;
23 |
24 | @JsonIgnore
25 | private String password;
26 |
27 | private Collection extends GrantedAuthority> authorities;
28 |
29 | public UserPrincipal(Long id, String name, String username, String email, String password, Collection extends GrantedAuthority> authorities) {
30 | this.id = id;
31 | this.name = name;
32 | this.username = username;
33 | this.email = email;
34 | this.password = password;
35 | this.authorities = authorities;
36 | }
37 |
38 | public static UserPrincipal create(User user) {
39 | List authorities = user.getRoles().stream().map(role ->
40 | new SimpleGrantedAuthority(role.getName().name())
41 | ).collect(Collectors.toList());
42 |
43 | return new UserPrincipal(
44 | user.getId(),
45 | user.getName(),
46 | user.getUsername(),
47 | user.getEmail(),
48 | user.getPassword(),
49 | authorities
50 | );
51 | }
52 |
53 | public Long getId() {
54 | return id;
55 | }
56 |
57 | public String getName() {
58 | return name;
59 | }
60 |
61 | public String getEmail() {
62 | return email;
63 | }
64 |
65 | @Override
66 | public String getUsername() {
67 | return username;
68 | }
69 |
70 | @Override
71 | public String getPassword() {
72 | return password;
73 | }
74 |
75 | @Override
76 | public Collection extends GrantedAuthority> getAuthorities() {
77 | return authorities;
78 | }
79 |
80 | @Override
81 | public boolean isAccountNonExpired() {
82 | return true;
83 | }
84 |
85 | @Override
86 | public boolean isAccountNonLocked() {
87 | return true;
88 | }
89 |
90 | @Override
91 | public boolean isCredentialsNonExpired() {
92 | return true;
93 | }
94 |
95 | @Override
96 | public boolean isEnabled() {
97 | return true;
98 | }
99 |
100 | @Override
101 | public boolean equals(Object o) {
102 | if (this == o) return true;
103 | if (o == null || getClass() != o.getClass()) return false;
104 | UserPrincipal that = (UserPrincipal) o;
105 | return Objects.equals(id, that.id);
106 | }
107 |
108 | @Override
109 | public int hashCode() {
110 |
111 | return Objects.hash(id);
112 | }
113 | }
114 |
--------------------------------------------------------------------------------
/polling-app-server/src/main/java/com/example/polls/service/PollService.java:
--------------------------------------------------------------------------------
1 | package com.example.polls.service;
2 |
3 | import com.example.polls.exception.BadRequestException;
4 | import com.example.polls.exception.ResourceNotFoundException;
5 | import com.example.polls.model.*;
6 | import com.example.polls.payload.PagedResponse;
7 | import com.example.polls.payload.PollRequest;
8 | import com.example.polls.payload.PollResponse;
9 | import com.example.polls.payload.VoteRequest;
10 | import com.example.polls.repository.PollRepository;
11 | import com.example.polls.repository.UserRepository;
12 | import com.example.polls.repository.VoteRepository;
13 | import com.example.polls.security.UserPrincipal;
14 | import com.example.polls.util.AppConstants;
15 | import com.example.polls.util.ModelMapper;
16 | import org.slf4j.Logger;
17 | import org.slf4j.LoggerFactory;
18 | import org.springframework.beans.factory.annotation.Autowired;
19 | import org.springframework.dao.DataIntegrityViolationException;
20 | import org.springframework.data.domain.Page;
21 | import org.springframework.data.domain.PageRequest;
22 | import org.springframework.data.domain.Pageable;
23 | import org.springframework.data.domain.Sort;
24 | import org.springframework.stereotype.Service;
25 |
26 | import java.time.Duration;
27 | import java.time.Instant;
28 | import java.util.Collections;
29 | import java.util.List;
30 | import java.util.Map;
31 | import java.util.function.Function;
32 | import java.util.stream.Collectors;
33 |
34 | @Service
35 | public class PollService {
36 |
37 | @Autowired
38 | private PollRepository pollRepository;
39 |
40 | @Autowired
41 | private VoteRepository voteRepository;
42 |
43 | @Autowired
44 | private UserRepository userRepository;
45 |
46 | private static final Logger logger = LoggerFactory.getLogger(PollService.class);
47 |
48 | public PagedResponse getAllPolls(UserPrincipal currentUser, int page, int size) {
49 | validatePageNumberAndSize(page, size);
50 |
51 | // Retrieve Polls
52 | Pageable pageable = PageRequest.of(page, size, Sort.Direction.DESC, "createdAt");
53 | Page polls = pollRepository.findAll(pageable);
54 |
55 | if(polls.getNumberOfElements() == 0) {
56 | return new PagedResponse<>(Collections.emptyList(), polls.getNumber(),
57 | polls.getSize(), polls.getTotalElements(), polls.getTotalPages(), polls.isLast());
58 | }
59 |
60 | // Map Polls to PollResponses containing vote counts and poll creator details
61 | List pollIds = polls.map(Poll::getId).getContent();
62 | Map choiceVoteCountMap = getChoiceVoteCountMap(pollIds);
63 | Map pollUserVoteMap = getPollUserVoteMap(currentUser, pollIds);
64 | Map creatorMap = getPollCreatorMap(polls.getContent());
65 |
66 | List pollResponses = polls.map(poll -> {
67 | return ModelMapper.mapPollToPollResponse(poll,
68 | choiceVoteCountMap,
69 | creatorMap.get(poll.getCreatedBy()),
70 | pollUserVoteMap == null ? null : pollUserVoteMap.getOrDefault(poll.getId(), null));
71 | }).getContent();
72 |
73 | return new PagedResponse<>(pollResponses, polls.getNumber(),
74 | polls.getSize(), polls.getTotalElements(), polls.getTotalPages(), polls.isLast());
75 | }
76 |
77 | public PagedResponse getPollsCreatedBy(String username, UserPrincipal currentUser, int page, int size) {
78 | validatePageNumberAndSize(page, size);
79 |
80 | User user = userRepository.findByUsername(username)
81 | .orElseThrow(() -> new ResourceNotFoundException("User", "username", username));
82 |
83 | // Retrieve all polls created by the given username
84 | Pageable pageable = PageRequest.of(page, size, Sort.Direction.DESC, "createdAt");
85 | Page polls = pollRepository.findByCreatedBy(user.getId(), pageable);
86 |
87 | if (polls.getNumberOfElements() == 0) {
88 | return new PagedResponse<>(Collections.emptyList(), polls.getNumber(),
89 | polls.getSize(), polls.getTotalElements(), polls.getTotalPages(), polls.isLast());
90 | }
91 |
92 | // Map Polls to PollResponses containing vote counts and poll creator details
93 | List pollIds = polls.map(Poll::getId).getContent();
94 | Map choiceVoteCountMap = getChoiceVoteCountMap(pollIds);
95 | Map pollUserVoteMap = getPollUserVoteMap(currentUser, pollIds);
96 |
97 | List pollResponses = polls.map(poll -> {
98 | return ModelMapper.mapPollToPollResponse(poll,
99 | choiceVoteCountMap,
100 | user,
101 | pollUserVoteMap == null ? null : pollUserVoteMap.getOrDefault(poll.getId(), null));
102 | }).getContent();
103 |
104 | return new PagedResponse<>(pollResponses, polls.getNumber(),
105 | polls.getSize(), polls.getTotalElements(), polls.getTotalPages(), polls.isLast());
106 | }
107 |
108 | public PagedResponse getPollsVotedBy(String username, UserPrincipal currentUser, int page, int size) {
109 | validatePageNumberAndSize(page, size);
110 |
111 | User user = userRepository.findByUsername(username)
112 | .orElseThrow(() -> new ResourceNotFoundException("User", "username", username));
113 |
114 | // Retrieve all pollIds in which the given username has voted
115 | Pageable pageable = PageRequest.of(page, size, Sort.Direction.DESC, "createdAt");
116 | Page userVotedPollIds = voteRepository.findVotedPollIdsByUserId(user.getId(), pageable);
117 |
118 | if (userVotedPollIds.getNumberOfElements() == 0) {
119 | return new PagedResponse<>(Collections.emptyList(), userVotedPollIds.getNumber(),
120 | userVotedPollIds.getSize(), userVotedPollIds.getTotalElements(),
121 | userVotedPollIds.getTotalPages(), userVotedPollIds.isLast());
122 | }
123 |
124 | // Retrieve all poll details from the voted pollIds.
125 | List pollIds = userVotedPollIds.getContent();
126 |
127 | Sort sort = Sort.by(Sort.Direction.DESC, "createdAt");
128 | List polls = pollRepository.findByIdIn(pollIds, sort);
129 |
130 | // Map Polls to PollResponses containing vote counts and poll creator details
131 | Map choiceVoteCountMap = getChoiceVoteCountMap(pollIds);
132 | Map pollUserVoteMap = getPollUserVoteMap(currentUser, pollIds);
133 | Map creatorMap = getPollCreatorMap(polls);
134 |
135 | List pollResponses = polls.stream().map(poll -> {
136 | return ModelMapper.mapPollToPollResponse(poll,
137 | choiceVoteCountMap,
138 | creatorMap.get(poll.getCreatedBy()),
139 | pollUserVoteMap == null ? null : pollUserVoteMap.getOrDefault(poll.getId(), null));
140 | }).collect(Collectors.toList());
141 |
142 | return new PagedResponse<>(pollResponses, userVotedPollIds.getNumber(), userVotedPollIds.getSize(), userVotedPollIds.getTotalElements(), userVotedPollIds.getTotalPages(), userVotedPollIds.isLast());
143 | }
144 |
145 |
146 | public Poll createPoll(PollRequest pollRequest) {
147 | Poll poll = new Poll();
148 | poll.setQuestion(pollRequest.getQuestion());
149 |
150 | pollRequest.getChoices().forEach(choiceRequest -> {
151 | poll.addChoice(new Choice(choiceRequest.getText()));
152 | });
153 |
154 | Instant now = Instant.now();
155 | Instant expirationDateTime = now.plus(Duration.ofDays(pollRequest.getPollLength().getDays()))
156 | .plus(Duration.ofHours(pollRequest.getPollLength().getHours()));
157 |
158 | poll.setExpirationDateTime(expirationDateTime);
159 |
160 | return pollRepository.save(poll);
161 | }
162 |
163 | public PollResponse getPollById(Long pollId, UserPrincipal currentUser) {
164 | Poll poll = pollRepository.findById(pollId).orElseThrow(
165 | () -> new ResourceNotFoundException("Poll", "id", pollId));
166 |
167 | // Retrieve Vote Counts of every choice belonging to the current poll
168 | List votes = voteRepository.countByPollIdGroupByChoiceId(pollId);
169 |
170 | Map choiceVotesMap = votes.stream()
171 | .collect(Collectors.toMap(ChoiceVoteCount::getChoiceId, ChoiceVoteCount::getVoteCount));
172 |
173 | // Retrieve poll creator details
174 | User creator = userRepository.findById(poll.getCreatedBy())
175 | .orElseThrow(() -> new ResourceNotFoundException("User", "id", poll.getCreatedBy()));
176 |
177 | // Retrieve vote done by logged in user
178 | Vote userVote = null;
179 | if(currentUser != null) {
180 | userVote = voteRepository.findByUserIdAndPollId(currentUser.getId(), pollId);
181 | }
182 |
183 | return ModelMapper.mapPollToPollResponse(poll, choiceVotesMap,
184 | creator, userVote != null ? userVote.getChoice().getId(): null);
185 | }
186 |
187 | public PollResponse castVoteAndGetUpdatedPoll(Long pollId, VoteRequest voteRequest, UserPrincipal currentUser) {
188 | Poll poll = pollRepository.findById(pollId)
189 | .orElseThrow(() -> new ResourceNotFoundException("Poll", "id", pollId));
190 |
191 | if(poll.getExpirationDateTime().isBefore(Instant.now())) {
192 | throw new BadRequestException("Sorry! This Poll has already expired");
193 | }
194 |
195 | User user = userRepository.getOne(currentUser.getId());
196 |
197 | Choice selectedChoice = poll.getChoices().stream()
198 | .filter(choice -> choice.getId().equals(voteRequest.getChoiceId()))
199 | .findFirst()
200 | .orElseThrow(() -> new ResourceNotFoundException("Choice", "id", voteRequest.getChoiceId()));
201 |
202 | Vote vote = new Vote();
203 | vote.setPoll(poll);
204 | vote.setUser(user);
205 | vote.setChoice(selectedChoice);
206 |
207 | try {
208 | vote = voteRepository.save(vote);
209 | } catch (DataIntegrityViolationException ex) {
210 | logger.info("User {} has already voted in Poll {}", currentUser.getId(), pollId);
211 | throw new BadRequestException("Sorry! You have already cast your vote in this poll");
212 | }
213 |
214 | //-- Vote Saved, Return the updated Poll Response now --
215 |
216 | // Retrieve Vote Counts of every choice belonging to the current poll
217 | List votes = voteRepository.countByPollIdGroupByChoiceId(pollId);
218 |
219 | Map choiceVotesMap = votes.stream()
220 | .collect(Collectors.toMap(ChoiceVoteCount::getChoiceId, ChoiceVoteCount::getVoteCount));
221 |
222 | // Retrieve poll creator details
223 | User creator = userRepository.findById(poll.getCreatedBy())
224 | .orElseThrow(() -> new ResourceNotFoundException("User", "id", poll.getCreatedBy()));
225 |
226 | return ModelMapper.mapPollToPollResponse(poll, choiceVotesMap, creator, vote.getChoice().getId());
227 | }
228 |
229 |
230 | private void validatePageNumberAndSize(int page, int size) {
231 | if(page < 0) {
232 | throw new BadRequestException("Page number cannot be less than zero.");
233 | }
234 |
235 | if(size > AppConstants.MAX_PAGE_SIZE) {
236 | throw new BadRequestException("Page size must not be greater than " + AppConstants.MAX_PAGE_SIZE);
237 | }
238 | }
239 |
240 | private Map getChoiceVoteCountMap(List pollIds) {
241 | // Retrieve Vote Counts of every Choice belonging to the given pollIds
242 | List votes = voteRepository.countByPollIdInGroupByChoiceId(pollIds);
243 |
244 | Map choiceVotesMap = votes.stream()
245 | .collect(Collectors.toMap(ChoiceVoteCount::getChoiceId, ChoiceVoteCount::getVoteCount));
246 |
247 | return choiceVotesMap;
248 | }
249 |
250 | private Map getPollUserVoteMap(UserPrincipal currentUser, List pollIds) {
251 | // Retrieve Votes done by the logged in user to the given pollIds
252 | Map pollUserVoteMap = null;
253 | if(currentUser != null) {
254 | List userVotes = voteRepository.findByUserIdAndPollIdIn(currentUser.getId(), pollIds);
255 |
256 | pollUserVoteMap = userVotes.stream()
257 | .collect(Collectors.toMap(vote -> vote.getPoll().getId(), vote -> vote.getChoice().getId()));
258 | }
259 | return pollUserVoteMap;
260 | }
261 |
262 | Map getPollCreatorMap(List polls) {
263 | // Get Poll Creator details of the given list of polls
264 | List creatorIds = polls.stream()
265 | .map(Poll::getCreatedBy)
266 | .distinct()
267 | .collect(Collectors.toList());
268 |
269 | List creators = userRepository.findByIdIn(creatorIds);
270 | Map creatorMap = creators.stream()
271 | .collect(Collectors.toMap(User::getId, Function.identity()));
272 |
273 | return creatorMap;
274 | }
275 | }
276 |
--------------------------------------------------------------------------------
/polling-app-server/src/main/java/com/example/polls/util/AppConstants.java:
--------------------------------------------------------------------------------
1 | package com.example.polls.util;
2 |
3 | public interface AppConstants {
4 | String DEFAULT_PAGE_NUMBER = "0";
5 | String DEFAULT_PAGE_SIZE = "30";
6 |
7 | int MAX_PAGE_SIZE = 50;
8 | }
9 |
--------------------------------------------------------------------------------
/polling-app-server/src/main/java/com/example/polls/util/ModelMapper.java:
--------------------------------------------------------------------------------
1 | package com.example.polls.util;
2 |
3 | import com.example.polls.model.Poll;
4 | import com.example.polls.model.User;
5 | import com.example.polls.payload.ChoiceResponse;
6 | import com.example.polls.payload.PollResponse;
7 | import com.example.polls.payload.UserSummary;
8 |
9 | import java.time.Instant;
10 | import java.util.List;
11 | import java.util.Map;
12 | import java.util.stream.Collectors;
13 |
14 | public class ModelMapper {
15 |
16 | public static PollResponse mapPollToPollResponse(Poll poll, Map choiceVotesMap, User creator, Long userVote) {
17 | PollResponse pollResponse = new PollResponse();
18 | pollResponse.setId(poll.getId());
19 | pollResponse.setQuestion(poll.getQuestion());
20 | pollResponse.setCreationDateTime(poll.getCreatedAt());
21 | pollResponse.setExpirationDateTime(poll.getExpirationDateTime());
22 | Instant now = Instant.now();
23 | pollResponse.setExpired(poll.getExpirationDateTime().isBefore(now));
24 |
25 | List choiceResponses = poll.getChoices().stream().map(choice -> {
26 | ChoiceResponse choiceResponse = new ChoiceResponse();
27 | choiceResponse.setId(choice.getId());
28 | choiceResponse.setText(choice.getText());
29 |
30 | if(choiceVotesMap.containsKey(choice.getId())) {
31 | choiceResponse.setVoteCount(choiceVotesMap.get(choice.getId()));
32 | } else {
33 | choiceResponse.setVoteCount(0);
34 | }
35 | return choiceResponse;
36 | }).collect(Collectors.toList());
37 |
38 | pollResponse.setChoices(choiceResponses);
39 | UserSummary creatorSummary = new UserSummary(creator.getId(), creator.getUsername(), creator.getName());
40 | pollResponse.setCreatedBy(creatorSummary);
41 |
42 | if(userVote != null) {
43 | pollResponse.setSelectedChoice(userVote);
44 | }
45 |
46 | long totalVotes = pollResponse.getChoices().stream().mapToLong(ChoiceResponse::getVoteCount).sum();
47 | pollResponse.setTotalVotes(totalVotes);
48 |
49 | return pollResponse;
50 | }
51 |
52 | }
53 |
--------------------------------------------------------------------------------
/polling-app-server/src/main/resources/application.properties:
--------------------------------------------------------------------------------
1 | ## Server Properties
2 | server.port= 8080
3 | server.compression.enabled=true
4 |
5 | ## Spring DATASOURCE (DataSourceAutoConfiguration & DataSourceProperties)
6 | spring.datasource.url= jdbc:mysql://localhost:3306/polling_app?useSSL=false&serverTimezone=UTC&useLegacyDatetimeCode=false
7 | spring.datasource.username= root
8 | spring.datasource.password= callicoder
9 |
10 |
11 | ## Hibernate Properties
12 | # The SQL dialect makes Hibernate generate better SQL for the chosen database
13 | spring.jpa.properties.hibernate.dialect = org.hibernate.dialect.MySQL5InnoDBDialect
14 | spring.jpa.hibernate.ddl-auto = update
15 |
16 | ## Hibernate Logging
17 | logging.level.org.hibernate.SQL= DEBUG
18 |
19 | # Initialize the datasource with available DDL and DML scripts
20 | spring.datasource.initialization-mode=always
21 | spring.jpa.defer-datasource-initialization= true
22 |
23 | ## Jackson Properties
24 | spring.jackson.serialization.WRITE_DATES_AS_TIMESTAMPS= false
25 | spring.jackson.time-zone= UTC
26 |
27 | ## App Properties
28 | app.jwtSecret= 9a02115a835ee03d5fb83cd8a468ea33e4090aaaec87f53c9fa54512bbef4db8dc656c82a315fa0c785c08b0134716b81ddcd0153d2a7556f2e154912cf5675f
29 | app.jwtExpirationInMs = 604800000
30 |
31 | # Comma separated list of allowed origins
32 | app.cors.allowedOrigins = http://localhost:3000
33 |
34 | ## Spring Profiles
35 | # spring.profiles.active=prod
36 |
--------------------------------------------------------------------------------
/polling-app-server/src/main/resources/data.sql:
--------------------------------------------------------------------------------
1 | INSERT IGNORE INTO roles(name) VALUES('ROLE_USER');
2 | INSERT IGNORE INTO roles(name) VALUES('ROLE_ADMIN');
--------------------------------------------------------------------------------
/polling-app-server/src/main/resources/db/migration/V1__schema.sql:
--------------------------------------------------------------------------------
1 | CREATE TABLE `users` (
2 | `id` bigint(20) NOT NULL AUTO_INCREMENT,
3 | `name` varchar(40) NOT NULL,
4 | `username` varchar(15) NOT NULL,
5 | `email` varchar(40) NOT NULL,
6 | `password` varchar(100) NOT NULL,
7 | `created_at` timestamp DEFAULT CURRENT_TIMESTAMP,
8 | `updated_at` timestamp DEFAULT CURRENT_TIMESTAMP,
9 | PRIMARY KEY (`id`),
10 | UNIQUE KEY `uk_users_username` (`username`),
11 | UNIQUE KEY `uk_users_email` (`email`)
12 | ) ENGINE=InnoDB DEFAULT CHARSET=utf8;
13 |
14 |
15 | CREATE TABLE `roles` (
16 | `id` bigint(20) NOT NULL AUTO_INCREMENT,
17 | `name` varchar(60) NOT NULL,
18 | PRIMARY KEY (`id`),
19 | UNIQUE KEY `uk_roles_name` (`name`)
20 | ) ENGINE=InnoDB AUTO_INCREMENT=4 DEFAULT CHARSET=utf8;
21 |
22 |
23 | CREATE TABLE `user_roles` (
24 | `user_id` bigint(20) NOT NULL,
25 | `role_id` bigint(20) NOT NULL,
26 | PRIMARY KEY (`user_id`,`role_id`),
27 | KEY `fk_user_roles_role_id` (`role_id`),
28 | CONSTRAINT `fk_user_roles_role_id` FOREIGN KEY (`role_id`) REFERENCES `roles` (`id`),
29 | CONSTRAINT `fk_user_roles_user_id` FOREIGN KEY (`user_id`) REFERENCES `users` (`id`)
30 | ) ENGINE=InnoDB DEFAULT CHARSET=utf8;
31 |
32 |
33 | CREATE TABLE `polls` (
34 | `id` bigint(20) NOT NULL AUTO_INCREMENT,
35 | `question` varchar(140) NOT NULL,
36 | `expiration_date_time` datetime NOT NULL,
37 | `created_at` timestamp DEFAULT CURRENT_TIMESTAMP,
38 | `updated_at` timestamp DEFAULT CURRENT_TIMESTAMP,
39 | `created_by` bigint(20) DEFAULT NULL,
40 | `updated_by` bigint(20) DEFAULT NULL,
41 | PRIMARY KEY (`id`)
42 | ) ENGINE=InnoDB DEFAULT CHARSET=utf8;
43 |
44 |
45 | CREATE TABLE `choices` (
46 | `id` bigint(20) NOT NULL AUTO_INCREMENT,
47 | `text` varchar(40) NOT NULL,
48 | `poll_id` bigint(20) NOT NULL,
49 | PRIMARY KEY (`id`),
50 | KEY `fk_choices_poll_id` (`poll_id`),
51 | CONSTRAINT `fk_choices_poll_id` FOREIGN KEY (`poll_id`) REFERENCES `polls` (`id`)
52 | ) ENGINE=InnoDB DEFAULT CHARSET=utf8;
53 |
54 |
55 | CREATE TABLE `votes` (
56 | `id` bigint(20) NOT NULL AUTO_INCREMENT,
57 | `user_id` bigint(20) NOT NULL,
58 | `poll_id` bigint(20) NOT NULL,
59 | `choice_id` bigint(20) NOT NULL,
60 | `created_at` timestamp DEFAULT CURRENT_TIMESTAMP,
61 | `updated_at` timestamp DEFAULT CURRENT_TIMESTAMP,
62 | PRIMARY KEY (`id`),
63 | KEY `fk_votes_user_id` (`user_id`),
64 | KEY `fk_votes_poll_id` (`poll_id`),
65 | KEY `fk_votes_choice_id` (`choice_id`),
66 | CONSTRAINT `fk_votes_user_id` FOREIGN KEY (`user_id`) REFERENCES `users` (`id`),
67 | CONSTRAINT `fk_votes_poll_id` FOREIGN KEY (`poll_id`) REFERENCES `polls` (`id`),
68 | CONSTRAINT `fk_votes_choice_id` FOREIGN KEY (`choice_id`) REFERENCES `choices` (`id`)
69 | ) ENGINE=InnoDB DEFAULT CHARSET=utf8;
70 |
--------------------------------------------------------------------------------
/polling-app-server/src/main/resources/db/migration/V2__default_roles.sql:
--------------------------------------------------------------------------------
1 | INSERT IGNORE INTO roles(name) VALUES('ROLE_USER');
2 | INSERT IGNORE INTO roles(name) VALUES('ROLE_ADMIN');
--------------------------------------------------------------------------------
/polling-app-server/src/test/java/com/example/polls/PollsApplicationTests.java:
--------------------------------------------------------------------------------
1 | package com.example.polls;
2 |
3 | import org.junit.jupiter.api.Test;
4 | import org.springframework.boot.test.context.SpringBootTest;
5 |
6 | @SpringBootTest
7 | public class PollsApplicationTests {
8 |
9 | @Test
10 | public void contextLoads() {
11 | }
12 |
13 | }
14 |
--------------------------------------------------------------------------------
/screenshot.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/callicoder/spring-security-react-ant-design-polls-app/362fad90cab17e76453b3b9e273c594de6ee3d7f/screenshot.png
--------------------------------------------------------------------------------