Skip to content

Commit

Permalink
Merge pull request #269 from Longwater1234/version_three
Browse files Browse the repository at this point in the history
Version three
  • Loading branch information
Longwater1234 authored Dec 12, 2024
2 parents 442b2a6 + b069c1a commit e276108
Show file tree
Hide file tree
Showing 50 changed files with 302 additions and 217 deletions.
4 changes: 2 additions & 2 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
# syntax=docker/dockerfile:1
# Build and Run docker image with --tag e.g. "wedemyserver"
FROM maven:3-eclipse-temurin-alpine AS build
FROM maven:3-eclipse-temurin-17-alpine AS build
WORKDIR /app
COPY pom.xml ./
COPY src ./src
RUN mvn clean -DskipTests package


FROM eclipse-temurin:jre-alpine AS runner
FROM eclipse-temurin:17-jre-alpine AS runner
WORKDIR /app
COPY --from=build /app/target/wedemyserver.jar /app
EXPOSE 9000
Expand Down
32 changes: 17 additions & 15 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
# WedemyServer

[![Static Badge](https://img.shields.io/badge/API_docs-v1.2-red)](https://longwater1234.github.io/WedemyServer/)
[![Static Badge](https://img.shields.io/badge/API_docs-v2.0-red)](https://longwater1234.github.io/WedemyServer/)
[![Maintenance](https://img.shields.io/badge/Maintained%3F-yes-green.svg)](https://github.com/Longwater1234/WedemyServer/graphs/commit-activity)
[![License: MIT](https://img.shields.io/github/license/Longwater1234/WedemyServer)](https://github.com/Longwater1234/WedemyServer/blob/master/LICENSE)
[![Static Badge](https://img.shields.io/badge/reference-help.md-orange)](HELP.md)

(Backend repo). Clone of Udemy, an e-learning platform, built using SpringBoot + Vue 3 + Typescript. With CreditCard and
(Backend repo). Clone of Udemy, an e-learning platform, built using SpringBoot 3 + Vue 3 + Typescript. With CreditCard and
PayPal checkout (both powered by **Braintree Payments**). Uses Spring Security & Spring Session Redis (via cookies[^1]
or sessionID Headers) for auth, instead of stateless JWT Tokens. CSRF protection is enabled. You can easily customize
these settings in [SecurityConfig](src/main/java/com/davistiba/wedemyserver/config/SecurityConfig.java). By default, the
Expand All @@ -14,13 +14,13 @@ app runs on port 9000.
## Frontend & Live Demo

Click to view [Frontend Repo](https://github.com/Longwater1234/WedemyClient) and live Demo built using Vue 3, Vite and
Typescript. However, you can still use any frontend stack with this project. See
the [API Docs](https://longwater1234.github.io/WedemyServer/) for this project.
Typescript. However, you may use any other frontend stack with this project. See
the [OpenAPI Docs](https://longwater1234.github.io/WedemyServer/) for this project.

## Requirements
## Minimum Requirements

- JDK 11 or newer
- MySQL 8.0 or newer (NOT compatible with MariaDB)
- Java 17 or newer
- MySQL 8.0.x or newer
- Redis Server (latest stable)
- [Google OAuth Credentials](https://developers.google.com/identity/gsi/web/guides/get-google-api-clientid) (for Google
Login)
Expand All @@ -32,7 +32,7 @@ the [API Docs](https://longwater1234.github.io/WedemyServer/) for this project.
You MUST set these variables on your Local or Cloud ENV before you launch this SpringBoot app. **💡TIP**: During
dev/test, you can pass them via `args`, OR store inside your IDE: e.g. In either Eclipse or IntelliJ IDE, in the top
toolbar, find the **"Run"** menu > **Edit/Run Configuration** > **Environment** > **Environmental Variables**. Add (+)
each key and its value, then click **Apply**. If using Docker CLI, follow this quick
each key and its value, then click **Apply**. If using Docker CLI, please follow this quick
[official guide.](https://docs.docker.com/engine/reference/commandline/run/#env)

```properties
Expand Down Expand Up @@ -61,7 +61,8 @@ in [BraintreeConfig](src/main/java/com/davistiba/wedemyserver/config/BraintreeCo
## Database Setup

Using any MySQL client, CREATE new database called `wedemy` (any name is OK), with charset `utf8mb4`. Then follow
carefully the rest of instructions in [HELP.md](HELP.md#database-setup-info), for both MySQL and Redis.
carefully instructions in [HELP.md](HELP.md#database-setup-info), for both MySQL and Redis. We recommend NOT to use
`root` account in prod for Db; create new user account with fewer privileges.

## Quick Start 🚀

Expand All @@ -77,21 +78,22 @@ java -jar target/wedemyserver.jar

### With Docker

I have attached [Dockerfile](Dockerfile) for the Spring server only. You will need to set up MySQL & Redis
separately. Refer to official Docker docs on how to pass Env variables.
I have attached [Dockerfile](Dockerfile) for the Springboot server only. You will need to set up MySQL &
Redis separately. Refer to official Docker docs on how to pass ENV variables listed above.

```bash
docker build -t wedemy-server .
docker run --name "wedemy" -d -p9000:9000 wedemy-server
docker build -t wedemyserver .
docker run --name "wedemy" -d -p9000:9000 wedemyserver
```

Tip💡 : If using Docker Desktop (latest), before starting container, you can fill in the ENV vars in the GUI directly.
**Tip** 💡 : If using Docker Desktop (latest), before starting container, you can fill in the ENV vars in the GUI
directly.
See [screenshot](src/main/resources/docker_env.PNG)

## Deploying your App 🌍

This App can be easily deployed within few minutes, straight from GitHub to your Cloud PaaS of choice. You can either
use the [Dockerfile](Dockerfile) provided, or as a pure Java app. Popular PaaS with CI/CD for Java
use the [Dockerfile](Dockerfile) provided, or as a pure Java app. Popular PaaS with CI/CD for Java (without Dockerfile)
include: Heroku, AWS ElasticBeanstalk, Google App Engine, Azure Web Apps. The following may **require** a Dockerfile:
Dokku, Railway, Render.com, Fly.io. Please note, you will also need a **separate** MySQL & Redis instance!

Expand Down
20 changes: 13 additions & 7 deletions pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -6,25 +6,25 @@
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.7.18</version>
<version>3.4.0</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.davistiba</groupId>
<artifactId>wedemyserver</artifactId>
<version>1.2</version>
<version>2.0.0</version>
<name>wedemyserver</name>
<description>Wedemy clone server, built with Spring Boot</description>
<url>https://github.com/longwater1234/WedemyServer</url>

<properties>
<java.version>11</java.version>
<maven.compiler.source>11</maven.compiler.source>
<maven.compiler.target>11</maven.compiler.target>
<java.version>17</java.version>
<maven.compiler.source>17</maven.compiler.source>
<maven.compiler.target>17</maven.compiler.target>
</properties>
<dependencies>
<dependency>
<groupId>javax.validation</groupId>
<artifactId>validation-api</artifactId>
<groupId>jakarta.validation</groupId>
<artifactId>jakarta.validation-api</artifactId>
</dependency>
<dependency>
<groupId>org.hibernate.validator</groupId>
Expand Down Expand Up @@ -136,6 +136,11 @@
<artifactId>braintree-java</artifactId>
<version>3.37.0</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-properties-migrator</artifactId>
<scope>runtime</scope>
</dependency>

<!-- UNCOMMENT TO ENABLE SWAGGER -->
<!-- <dependency>
Expand Down Expand Up @@ -184,6 +189,7 @@
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<version>${project.parent.version}</version>
<configuration>
<excludes>
<exclude>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ public static void main(String[] args) {
}

@Bean
// important for Heroku
public static ConfigureRedisAction configureRedisAction() {
return ConfigureRedisAction.NO_OP;
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
package com.davistiba.wedemyserver.config;

import com.davistiba.wedemyserver.dto.UserDTO;
import com.davistiba.wedemyserver.models.User;
import com.davistiba.wedemyserver.service.MyUserDetailsService;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.modelmapper.ModelMapper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
Expand All @@ -14,9 +16,6 @@
import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
import org.springframework.stereotype.Component;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.HashMap;
Expand Down Expand Up @@ -44,15 +43,14 @@ public CustomAuthSuccessHandler(MyUserDetailsService myUserDetailsService) {
public void onAuthenticationSuccess(HttpServletRequest request,
HttpServletResponse response,
Authentication auth) throws IOException, ServletException {
if (auth.getPrincipal() instanceof OidcUser) {
if (auth.getPrincipal() instanceof OidcUser oidcUser) {
//if Google Login
OidcUser oidcUser = (OidcUser) auth.getPrincipal();
myUserDetailsService.processOAuthPostLogin(oidcUser, request.getSession());
response.sendRedirect(FRONTEND_URL);
return;
}
User loggedInUser = (User) auth.getPrincipal();
UserDTO userInfo = modelMapper.map(loggedInUser, UserDTO.class);
MainUserDetails loggedInUser = (MainUserDetails) auth.getPrincipal();
UserDTO userInfo = modelMapper.map(loggedInUser.getUser(), UserDTO.class);

request.getSession().setAttribute(MyUserDetailsService.USERID, userInfo.getId());
Map<String, Object> authResponse = new HashMap<>();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,9 @@
import com.davistiba.wedemyserver.dto.LoginRequest;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.json.JsonMapper;
import jakarta.servlet.annotation.WebFilter;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.AuthenticationServiceException;
Expand All @@ -13,10 +16,6 @@
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import org.springframework.stereotype.Component;

import javax.servlet.annotation.WebFilter;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

/**
* Custom handling of Login by JSON Post request
*/
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
package com.davistiba.wedemyserver.config;

import com.davistiba.wedemyserver.models.User;
import com.fasterxml.jackson.annotation.JsonIgnore;
import lombok.Getter;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;

import java.io.Serial;
import java.io.Serializable;
import java.util.Collection;
import java.util.Collections;
import java.util.Objects;

@Getter
public class MainUserDetails implements UserDetails, Serializable {

@Serial
private static final long serialVersionUID = 3733936374876008250L;

private final User user;

public MainUserDetails(User user) {
this.user = user;
}

@Override
@JsonIgnore
public Collection<? extends GrantedAuthority> getAuthorities() {
SimpleGrantedAuthority authority = new SimpleGrantedAuthority(user.getUserRole());
return Collections.singletonList(authority);
}

@Override
@JsonIgnore
public String getPassword() {
return user.getPassword();
}

@Override
@JsonIgnore
public String getUsername() {
return user.getEmail();
}

@Override
@JsonIgnore
public boolean isAccountNonExpired() {
return true;
}

@Override
@JsonIgnore
public boolean isAccountNonLocked() {
return true;
}

@Override
@JsonIgnore
public boolean isCredentialsNonExpired() {
return true;
}

@Override
@JsonIgnore
public boolean isEnabled() {
return user.getEnabled();
}

@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
MainUserDetails that = (MainUserDetails) o;
return Objects.equals(user.getEmail(), that.user.getEmail());
}

@Override
public int hashCode() {
return Objects.hashCode(user.getEmail());
}
}
40 changes: 24 additions & 16 deletions src/main/java/com/davistiba/wedemyserver/config/SecurityConfig.java
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,13 @@
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Profile;
import org.springframework.core.annotation.Order;
import org.springframework.http.HttpMethod;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.Customizer;
import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer;
Expand All @@ -22,7 +23,7 @@

@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled = true)
@EnableMethodSecurity(jsr250Enabled = true, securedEnabled = true)
public class SecurityConfig {

@Bean
Expand Down Expand Up @@ -53,22 +54,29 @@ public HttpSessionIdResolver sessionIdResolver() {
@Bean
@Order(1)
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http.cors().and().httpBasic(Customizer.withDefaults())
.oauth2Login().userInfoEndpoint().oidcUserService(googleOauthService)
.and().successHandler(successHandler)
.and().authorizeHttpRequests((authz) ->
authz.antMatchers("/index.html", "/", "/auth/**", "/favicon.ico", "/login/**").permitAll()
.antMatchers(HttpMethod.GET, "/courses/**", "/objectives/**", "/lessons/**", "/reviews/**").permitAll()
.antMatchers("/profile/**", "/user/**").hasAuthority(UserRole.ROLE_STUDENT.name())
.antMatchers(HttpMethod.GET, "/swagger-ui/**", "/swagger-ui.html", "/v3/api-docs/**").permitAll()
.antMatchers("/admin/**").hasAuthority(UserRole.ROLE_ADMIN.name())
return http.cors(Customizer.withDefaults()).httpBasic(Customizer.withDefaults())
.oauth2Login(x -> x.userInfoEndpoint(config -> config.oidcUserService(googleOauthService)).successHandler(successHandler))
.csrf(c -> c.csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse()).ignoringRequestMatchers("/oauth2/**", "/auth/**"))
.sessionManagement(s -> s.maximumSessions(2))
.authorizeHttpRequests((authz) ->
authz.requestMatchers("/index.html", "/", "/auth/**", "/favicon.ico", "/login/**").permitAll()
.requestMatchers(HttpMethod.GET, "/courses/**", "/objectives/**", "/lessons/**", "/reviews/**").permitAll()
.requestMatchers("/profile/**", "/user/**").hasAuthority(UserRole.ROLE_STUDENT.name())
.requestMatchers(HttpMethod.GET, "/swagger-ui/**", "/swagger-ui.html", "/v3/api-docs/**").permitAll()
.requestMatchers("/admin/**").hasAuthority(UserRole.ROLE_ADMIN.name())
.anyRequest().authenticated())
.apply(new MyCustomFilterSetup(successHandler));
.with(new MyCustomFilterSetup(successHandler), x -> {
}).build();
}

//SESSION and CSRF (you may disable CSRF)
return http.csrf().csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse())
.ignoringAntMatchers("/oauth2/**", "/auth/**")
.and().sessionManagement(s -> s.maximumSessions(2)).build();
@Bean
@Order(1)
@Profile(value = "debug")
public SecurityFilterChain securityFilterChainDebug(HttpSecurity http) throws Exception {
// Disable CSRF and Allow all paths
http.csrf(AbstractHttpConfigurer::disable)
.authorizeHttpRequests(c -> c.anyRequest().permitAll());
return http.build();
}

@Bean
Expand Down
Loading

0 comments on commit e276108

Please sign in to comment.