From de3d80ce3ea20a869d700c3238020d44059de099 Mon Sep 17 00:00:00 2001 From: realtradam Date: Sat, 27 Jul 2024 02:00:57 -0400 Subject: working login and auth --- .../blog/web/controllers/ArticleController.java | 26 +++---- .../com/blog/web/controllers/AuthController.java | 10 ++- .../src/main/java/com/blog/web/dto/ArticleDto.java | 4 +- .../java/com/blog/web/dto/ArticlePublicDto.java | 4 +- .../main/java/com/blog/web/dto/UserPublicDto.java | 22 ++++++ .../src/main/java/com/blog/web/models/Article.java | 4 +- .../java/com/blog/web/security/CorsConfig.java | 21 +++++ .../java/com/blog/web/security/SecurityConfig.java | 9 ++- frontend/src/components/Layout.tsx | 44 +++++++---- frontend/src/pages/Article.tsx | 25 ------ frontend/src/pages/Login.tsx | 73 ----------------- frontend/src/pages/Register.tsx | 86 -------------------- frontend/src/pages/articles/Article.tsx | 25 ++++++ frontend/src/pages/articles/New.tsx | 83 ++++++++++++++++++++ frontend/src/pages/auth/Login.tsx | 91 ++++++++++++++++++++++ frontend/src/pages/auth/Register.tsx | 86 ++++++++++++++++++++ frontend/src/routes/index.tsx | 19 +++-- 17 files changed, 404 insertions(+), 228 deletions(-) create mode 100644 backend/src/main/java/com/blog/web/dto/UserPublicDto.java create mode 100644 backend/src/main/java/com/blog/web/security/CorsConfig.java delete mode 100644 frontend/src/pages/Article.tsx delete mode 100644 frontend/src/pages/Login.tsx delete mode 100644 frontend/src/pages/Register.tsx create mode 100644 frontend/src/pages/articles/Article.tsx create mode 100644 frontend/src/pages/articles/New.tsx create mode 100644 frontend/src/pages/auth/Login.tsx create mode 100644 frontend/src/pages/auth/Register.tsx diff --git a/backend/src/main/java/com/blog/web/controllers/ArticleController.java b/backend/src/main/java/com/blog/web/controllers/ArticleController.java index 7ffa2fe..ec8af85 100644 --- a/backend/src/main/java/com/blog/web/controllers/ArticleController.java +++ b/backend/src/main/java/com/blog/web/controllers/ArticleController.java @@ -7,16 +7,14 @@ import com.blog.web.models.UserEntity; import com.blog.web.services.ArticleService; import com.blog.web.services.UserService; import jakarta.validation.Valid; -import org.springframework.stereotype.Controller; import org.springframework.ui.Model; import org.springframework.validation.BindingResult; import org.springframework.web.bind.annotation.*; import java.time.LocalDateTime; import java.util.HashSet; -import java.util.List; -@CrossOrigin(origins = "http://localhost:5173", allowCredentials = "true") +@CrossOrigin(origins = "http://localhost:5173", allowCredentials = "true", allowedHeaders = "*") @RestController @RequestMapping("/api/v1") public class ArticleController { @@ -31,7 +29,7 @@ public class ArticleController { @GetMapping("/get") public Article getMethod() { return new Article( - 5, + 5L, "blah", "blah", "blah", @@ -42,12 +40,8 @@ public class ArticleController { } @GetMapping("/articles") - public HashSet listArticles(Model model) { + public HashSet listArticles() { HashSet articles = new HashSet(articleService.findAllArticles()); - //UserEntity user = userService.getLoggedInUser().orElse(new UserEntity()); - //model.addAttribute("user", user); - //model.addAttribute("articles", articles); - //return "index"; return articles; } @@ -68,20 +62,18 @@ public class ArticleController { return "articles/new"; }*/ - @PostMapping("/articles/new") - public String saveArticle(@Valid @ModelAttribute("article") ArticleDto articleDto, BindingResult result, Model model) { + @PostMapping("/article/new") + public String saveArticle(@Valid @ModelAttribute("article") ArticleDto articleDto, BindingResult result) { // if non-authenticated in user tries to create an article // redirect them to login page UserEntity user = userService.getLoggedInUser().orElse(null); if (user == null) { - return "redirect:/userlogin"; - } else if (result.hasErrors()) { - model.addAttribute("article", articleDto); - return "articles/new"; - } else { + return "redirect:/login"; + } else if (!result.hasErrors()) { articleService.saveArticle(articleDto); - return "redirect:/articles"; + return "redirect:/"; } + return ""; } @GetMapping("/articles/delete/{articleId}") diff --git a/backend/src/main/java/com/blog/web/controllers/AuthController.java b/backend/src/main/java/com/blog/web/controllers/AuthController.java index a870086..4da346b 100644 --- a/backend/src/main/java/com/blog/web/controllers/AuthController.java +++ b/backend/src/main/java/com/blog/web/controllers/AuthController.java @@ -1,15 +1,15 @@ package com.blog.web.controllers; import com.blog.web.dto.RegistrationDto; +import com.blog.web.dto.UserPublicDto; import com.blog.web.models.UserEntity; import com.blog.web.services.UserService; import jakarta.validation.Valid; import org.apache.commons.lang3.StringUtils; -import org.springframework.ui.Model; import org.springframework.validation.BindingResult; import org.springframework.web.bind.annotation.*; -@CrossOrigin(origins = "http://localhost:5173", allowCredentials = "true") +@CrossOrigin(origins = "http://localhost:5173", allowCredentials = "true", allowedHeaders = "*") @RestController @RequestMapping("/api/v1") public class AuthController { @@ -37,4 +37,10 @@ public class AuthController { } return user; } + + @GetMapping("/profile") + public UserPublicDto profile() { + final UserEntity user = userService.getLoggedInUser().orElse(null); + return new UserPublicDto(user); + } } diff --git a/backend/src/main/java/com/blog/web/dto/ArticleDto.java b/backend/src/main/java/com/blog/web/dto/ArticleDto.java index d275f3b..beede12 100644 --- a/backend/src/main/java/com/blog/web/dto/ArticleDto.java +++ b/backend/src/main/java/com/blog/web/dto/ArticleDto.java @@ -110,5 +110,7 @@ public class ArticleDto { this.createdBy = createdBy; } - public String getUsername() { return createdBy.getUsername(); } + public String getUsername() { + return createdBy.getUsername(); + } } diff --git a/backend/src/main/java/com/blog/web/dto/ArticlePublicDto.java b/backend/src/main/java/com/blog/web/dto/ArticlePublicDto.java index 3ced6d2..ecf27eb 100644 --- a/backend/src/main/java/com/blog/web/dto/ArticlePublicDto.java +++ b/backend/src/main/java/com/blog/web/dto/ArticlePublicDto.java @@ -50,7 +50,7 @@ public class ArticlePublicDto { return title; } - public void setTitle( String title) { + public void setTitle(String title) { this.title = title; } @@ -66,7 +66,7 @@ public class ArticlePublicDto { return content; } - public void setContent( String content) { + public void setContent(String content) { this.content = content; } diff --git a/backend/src/main/java/com/blog/web/dto/UserPublicDto.java b/backend/src/main/java/com/blog/web/dto/UserPublicDto.java new file mode 100644 index 0000000..547a7b2 --- /dev/null +++ b/backend/src/main/java/com/blog/web/dto/UserPublicDto.java @@ -0,0 +1,22 @@ +package com.blog.web.dto; + +import com.blog.web.models.UserEntity; + +public class UserPublicDto { + private String username; + + public UserPublicDto() { + } + + public UserPublicDto(String username) { + this.username = username; + } + + public UserPublicDto(UserEntity user) { + this.username = user.getUsername(); + } + + public String getUsername() { + return username; + } +} diff --git a/backend/src/main/java/com/blog/web/models/Article.java b/backend/src/main/java/com/blog/web/models/Article.java index b54907a..78ad668 100644 --- a/backend/src/main/java/com/blog/web/models/Article.java +++ b/backend/src/main/java/com/blog/web/models/Article.java @@ -11,7 +11,7 @@ import java.time.LocalDateTime; public class Article { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) - private long id; + private Long id; private String title; private String photoUrl; private String content; @@ -23,7 +23,7 @@ public class Article { @JoinColumn(name = "created_by", nullable = false) private UserEntity createdBy; - public Article(long id, String title, String photoUrl, String content, UserEntity createdBy, LocalDateTime createdOn, LocalDateTime updatedOn) { + public Article(Long id, String title, String photoUrl, String content, UserEntity createdBy, LocalDateTime createdOn, LocalDateTime updatedOn) { this.id = id; this.title = title; this.photoUrl = photoUrl; diff --git a/backend/src/main/java/com/blog/web/security/CorsConfig.java b/backend/src/main/java/com/blog/web/security/CorsConfig.java new file mode 100644 index 0000000..55db15a --- /dev/null +++ b/backend/src/main/java/com/blog/web/security/CorsConfig.java @@ -0,0 +1,21 @@ +package com.blog.web.security; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.web.servlet.config.annotation.CorsRegistry; +import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; + +@Configuration +public class CorsConfig { + + // Configures CORS for the application + @Bean + public WebMvcConfigurer corsConfigurer() { + return new WebMvcConfigurer() { + @Override + public void addCorsMappings(CorsRegistry registry) { + registry.addMapping("/**").allowedOrigins("http://localhost:5173").allowedMethods("GET", "POST", "PUT", "DELETE").allowedHeaders("*").allowCredentials(true); + } + }; + } +} diff --git a/backend/src/main/java/com/blog/web/security/SecurityConfig.java b/backend/src/main/java/com/blog/web/security/SecurityConfig.java index 2be6909..e562041 100644 --- a/backend/src/main/java/com/blog/web/security/SecurityConfig.java +++ b/backend/src/main/java/com/blog/web/security/SecurityConfig.java @@ -2,6 +2,7 @@ package com.blog.web.security; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; +import org.springframework.security.config.Customizer; import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder; import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity; import org.springframework.security.config.annotation.web.builders.HttpSecurity; @@ -9,6 +10,11 @@ import org.springframework.security.config.annotation.web.configuration.EnableWe import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.security.web.SecurityFilterChain; +import org.springframework.web.cors.CorsConfiguration; +import org.springframework.web.cors.CorsConfigurationSource; +import org.springframework.web.cors.UrlBasedCorsConfigurationSource; + +import java.util.Arrays; @Configuration @EnableWebSecurity @@ -28,11 +34,12 @@ public class SecurityConfig { @Bean public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { // disabling csrf leaves us vulnerable, in a real production app do not do this - http.csrf(c -> c.disable()).cors(c -> c.disable()).authorizeHttpRequests(auths -> auths.anyRequest().permitAll()).formLogin(form -> form.loginPage("/login").usernameParameter("username").passwordParameter("password").defaultSuccessUrl("/").loginProcessingUrl("/userlogin").failureUrl("/userlogin?error=true").permitAll()).logout(logout -> logout.logoutUrl("/logout").logoutSuccessUrl("/articles")); + http.csrf(c -> c.disable()).cors(Customizer.withDefaults()).authorizeHttpRequests(auths -> auths.anyRequest().permitAll()).formLogin(form -> form.loginPage("/api/v1/login").usernameParameter("username").passwordParameter("password").defaultSuccessUrl("/").loginProcessingUrl("/api/v1/login").failureUrl("/login?error=true").permitAll()).logout(logout -> logout.logoutUrl("/api/v1/logout").logoutSuccessUrl("/articles")); return http.build(); } public void configure(AuthenticationManagerBuilder builder) throws Exception { builder.userDetailsService(userDetailsService).passwordEncoder(passwordEncoder()); } + } diff --git a/frontend/src/components/Layout.tsx b/frontend/src/components/Layout.tsx index f7b8591..cdd3929 100644 --- a/frontend/src/components/Layout.tsx +++ b/frontend/src/components/Layout.tsx @@ -1,8 +1,33 @@ import { Outlet } from "react-router-dom"; +import { useState, useEffect } from "react"; export default function Layout() { + const [user, setUser] = useState(null); + + useEffect(() => { + const url = `${import.meta.env.VITE_API_TITLE}/api/v1/profile`; + fetch(url, { + credentials: 'include', + method: 'get', + }).then((response) => { + if (response.ok) { + return response.json(); + } + throw new Error("Network response was not ok."); + }).then((response) => { setUser(response.username); console.log(response.username) }); + }, [user]); + + const logout = () => { + fetch(`${import.meta.env.VITE_API_TITLE}/api/v1/logout`, { + credentials: 'include', + method: 'get', + }).then(() => { + setUser(null); + }); + } + return( <>
@@ -11,14 +36,7 @@ export default function Layout()
{/*th:if="${(user == null || user.username == null)}"*/} - - ☕Spring! - -{/*th:if="${!(user == null || user.username == null)}"*/} - -{/*th:text="'Logged in as: ' + ${user.username}"*/} - ☕ - +☕ { user === null ? "Spring!" : user }
    @@ -26,16 +44,14 @@ export default function Layout() HOME
  • - NEW + NEW
  • REGISTER
  • -
  • -{/*th:if="${(user == null || user.username == null)}"*/} - LOGIN -{/*th:if="${!(user == null || user.username == null)}"*/} - LOGOUT +
  • { user === null ? + LOGIN : + }
{/*th:action="@{/articles/search}"*/} diff --git a/frontend/src/pages/Article.tsx b/frontend/src/pages/Article.tsx deleted file mode 100644 index b367811..0000000 --- a/frontend/src/pages/Article.tsx +++ /dev/null @@ -1,25 +0,0 @@ -import { useState, useEffect } from "react"; -import { useParams } from "react-router-dom"; - -export default function Article () { - const { id } = useParams(); - const [articleData, setArticleData] = useState(); - - useEffect(() => { - const url = `${import.meta.env.VITE_API_TITLE}/api/v1/article/${id}`; - fetch(url).then((response) => { - if (response.ok) { - return response.json(); - } - throw new Error("Network response was not ok."); - }).then((response) => setArticleData(response)); //.catch(() => navigate("/")); - }, [id]); - - return( - <> -

{articleData?.title}

-
{articleData?.content}
- - ); - -} diff --git a/frontend/src/pages/Login.tsx b/frontend/src/pages/Login.tsx deleted file mode 100644 index 317fdb9..0000000 --- a/frontend/src/pages/Login.tsx +++ /dev/null @@ -1,73 +0,0 @@ -import { useState, useEffect } from "react"; -import { useParams, useNavigate } from "react-router-dom"; - -export default function Article () { - const navigate = useNavigate(); - -const handleSubmit = async (e: FormEvent) => { - e.preventDefault(); //stops submit from happening - - const target = e.target as typeof e.target & { - username: { value: string }; - email: { value: string }; - password: { value: string }; - }; - - const formData = new FormData(); - formData.append('username', target.username.value); - formData.append('password', target.password.value); - - const response = await fetch(`${import.meta.env.VITE_API_TITLE}/api/v1/register`, { - credentials: 'include', - method: 'post', - body: formData, - }); - if(response.ok) { - navigate("/"); - } - else { - alert("error"); - } - }; - - return( - <> -
Invalid Username/Password
-
You have been logged out
- -
-
-
-
- - -
-
-
-
- - -
-
-
-
- - -
-
- - ); - -} diff --git a/frontend/src/pages/Register.tsx b/frontend/src/pages/Register.tsx deleted file mode 100644 index 14ceea4..0000000 --- a/frontend/src/pages/Register.tsx +++ /dev/null @@ -1,86 +0,0 @@ -import { FormEvent } from "react"; -import { useNavigate } from 'react-router-dom'; - -export default function Register () { - const navigate = useNavigate(); - -const handleSubmit = async (e: FormEvent) => { - e.preventDefault(); //stops submit from happening - - const target = e.target as typeof e.target & { - username: { value: string }; - email: { value: string }; - password: { value: string }; - }; - - const formData = new FormData(); - formData.append('username', target.username.value); - formData.append('email', target.email.value); - formData.append('password', target.password.value); - - const response = await fetch(`${import.meta.env.VITE_API_TITLE}/api/v1/register`, { - credentials: 'include', - method: 'post', - body: formData, - }); - if(response.ok) { - navigate("/login"); - } - else { - alert("error"); - } - }; - - return( - <> -
-
Username or Email already exists
-
-
-
- - -

Please fill out this field.

-
-
- - -

Please fill out this field.

-
-
-
-
- - -

Please fill out this field.

-
-
-
-
- - -
-
- - ); - -} diff --git a/frontend/src/pages/articles/Article.tsx b/frontend/src/pages/articles/Article.tsx new file mode 100644 index 0000000..b367811 --- /dev/null +++ b/frontend/src/pages/articles/Article.tsx @@ -0,0 +1,25 @@ +import { useState, useEffect } from "react"; +import { useParams } from "react-router-dom"; + +export default function Article () { + const { id } = useParams(); + const [articleData, setArticleData] = useState(); + + useEffect(() => { + const url = `${import.meta.env.VITE_API_TITLE}/api/v1/article/${id}`; + fetch(url).then((response) => { + if (response.ok) { + return response.json(); + } + throw new Error("Network response was not ok."); + }).then((response) => setArticleData(response)); //.catch(() => navigate("/")); + }, [id]); + + return( + <> +

{articleData?.title}

+
{articleData?.content}
+ + ); + +} diff --git a/frontend/src/pages/articles/New.tsx b/frontend/src/pages/articles/New.tsx new file mode 100644 index 0000000..3402f22 --- /dev/null +++ b/frontend/src/pages/articles/New.tsx @@ -0,0 +1,83 @@ +import { FormEvent } from "react"; +import { useNavigate } from "react-router-dom"; + +export default function NewArticle () { + const navigate = useNavigate(); + + const handleSubmit = async (e: FormEvent) => { + e.preventDefault(); //stops submit from happening + + const target = e.target as typeof e.target & { + title: { value: string }; + photoUrl: { value: string }; + content: { value: string }; + }; + + const formData = new FormData(); + formData.append('title', target.title.value); + formData.append('photoUrl', target.photoUrl.value); + formData.append('content', target.content.value); + + const response = await fetch(`${import.meta.env.VITE_API_TITLE}/api/v1/article/new`, { + credentials: 'include', + method: 'post', + body: formData, + }); + if(response.ok) { + navigate("/"); + } + else { + console.log(response); + alert("check console for error"); + } + }; + return( + <> +
+
+
+
+ + +

Please fill out this field.

+
+
+ + +

Please fill out this field.

+
+
+
+
+ + +

Please fill out this field.

+
+
+
+
+ + +
+
+ + ); +} diff --git a/frontend/src/pages/auth/Login.tsx b/frontend/src/pages/auth/Login.tsx new file mode 100644 index 0000000..5a1e858 --- /dev/null +++ b/frontend/src/pages/auth/Login.tsx @@ -0,0 +1,91 @@ +import { FormEvent } from "react"; +import { useNavigate } from "react-router-dom"; + +//type setUser = { setUser: { func: React.Dispatch> } }; +type user = { set: React.Dispatch>, value: string | null }; + +export default function Login ({user}: {user: user}) { + const navigate = useNavigate(); + +const handleSubmit = async (e: FormEvent) => { + e.preventDefault(); //stops submit from happening + + const target = e.target as typeof e.target & { + username: { value: string }; + email: { value: string }; + password: { value: string }; + }; + + const formData = new FormData(); + formData.append('username', target.username.value); + formData.append('password', target.password.value); + + const response = await fetch(`${import.meta.env.VITE_API_TITLE}/api/v1/login`, { + credentials: 'include', + method: 'post', + body: formData, + }).then((res) => { + if(res.ok) { + const url = `${import.meta.env.VITE_API_TITLE}/api/v1/profile`; + fetch(url, { + credentials: 'include', + method: 'get', + }).then((response) => { + if (response.ok) { + return response.json(); + } + throw new Error("Network response was not ok."); + }).then((response) => { + user.set(response.username); + console.log("USER:"); + console.log(user); + console.log(user.value); + console.log(response.username); + navigate("/"); + }); + } + else { + console.log(response); + alert("check console for error"); + } + }); + }; + + return( + <> +
+
+
+
+ + +
+
+
+
+ + +
+
+
+
+ + +
+
+ + ); + +} diff --git a/frontend/src/pages/auth/Register.tsx b/frontend/src/pages/auth/Register.tsx new file mode 100644 index 0000000..14ceea4 --- /dev/null +++ b/frontend/src/pages/auth/Register.tsx @@ -0,0 +1,86 @@ +import { FormEvent } from "react"; +import { useNavigate } from 'react-router-dom'; + +export default function Register () { + const navigate = useNavigate(); + +const handleSubmit = async (e: FormEvent) => { + e.preventDefault(); //stops submit from happening + + const target = e.target as typeof e.target & { + username: { value: string }; + email: { value: string }; + password: { value: string }; + }; + + const formData = new FormData(); + formData.append('username', target.username.value); + formData.append('email', target.email.value); + formData.append('password', target.password.value); + + const response = await fetch(`${import.meta.env.VITE_API_TITLE}/api/v1/register`, { + credentials: 'include', + method: 'post', + body: formData, + }); + if(response.ok) { + navigate("/login"); + } + else { + alert("error"); + } + }; + + return( + <> +
+
Username or Email already exists
+
+
+
+ + +

Please fill out this field.

+
+
+ + +

Please fill out this field.

+
+
+
+
+ + +

Please fill out this field.

+
+
+
+
+ + +
+
+ + ); + +} diff --git a/frontend/src/routes/index.tsx b/frontend/src/routes/index.tsx index 6d7ffb1..e77421c 100644 --- a/frontend/src/routes/index.tsx +++ b/frontend/src/routes/index.tsx @@ -1,20 +1,29 @@ import { BrowserRouter as Router, Routes, Route } from "react-router-dom"; +import { useState } from "react"; import Home from "../pages/Home"; import Layout from "../components/Layout"; -import Article from "../pages/Article"; -import Register from "../pages/Register"; -import Login from "../pages/Login"; +import Article from "../pages/articles/Article"; +import NewArticle from "../pages/articles/New"; +import Register from "../pages/auth/Register"; +import Login from "../pages/auth/Login"; + +type user = { set: React.Dispatch>, value: string | null }; export default function Index() { + const [user, setUser] = useState(null); + + const userProp: user = { set: setUser, value: user }; + return (<> - }> + }> } /> } /> + } /> } /> - } /> + } /> -- cgit v1.2.3