summaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authorrealtradam <[email protected]>2024-07-27 02:00:57 -0400
committerrealtradam <[email protected]>2024-07-27 02:00:57 -0400
commitde3d80ce3ea20a869d700c3238020d44059de099 (patch)
treeef849326ea4922530990d5dc29cb8ff3532e82fe
parent6b342f97f6a605b7e1fe34584abbbf962ca39b7c (diff)
downloadspring-blog-de3d80ce3ea20a869d700c3238020d44059de099.tar.gz
spring-blog-de3d80ce3ea20a869d700c3238020d44059de099.zip
working login and auth
-rw-r--r--backend/src/main/java/com/blog/web/controllers/ArticleController.java26
-rw-r--r--backend/src/main/java/com/blog/web/controllers/AuthController.java10
-rw-r--r--backend/src/main/java/com/blog/web/dto/ArticleDto.java4
-rw-r--r--backend/src/main/java/com/blog/web/dto/ArticlePublicDto.java4
-rw-r--r--backend/src/main/java/com/blog/web/dto/UserPublicDto.java22
-rw-r--r--backend/src/main/java/com/blog/web/models/Article.java4
-rw-r--r--backend/src/main/java/com/blog/web/security/CorsConfig.java21
-rw-r--r--backend/src/main/java/com/blog/web/security/SecurityConfig.java9
-rw-r--r--frontend/src/components/Layout.tsx44
-rw-r--r--frontend/src/pages/articles/Article.tsx (renamed from frontend/src/pages/Article.tsx)0
-rw-r--r--frontend/src/pages/articles/New.tsx83
-rw-r--r--frontend/src/pages/auth/Login.tsx (renamed from frontend/src/pages/Login.tsx)44
-rw-r--r--frontend/src/pages/auth/Register.tsx (renamed from frontend/src/pages/Register.tsx)0
-rw-r--r--frontend/src/routes/index.tsx19
14 files changed, 233 insertions, 57 deletions
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<ArticlePublicDto> listArticles(Model model) {
+ public HashSet<ArticlePublicDto> listArticles() {
HashSet<ArticlePublicDto> articles = new HashSet<ArticlePublicDto>(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<string | null>(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(
<>
<div className="w-full bg-gray-200 min-h-screen font-sans leading-normal tracking-normal">
@@ -11,14 +36,7 @@ export default function Layout()
<div className="container mx-auto flex items-center">
<div className="flex text-white font-extrabold">
{/*th:if="${(user == null || user.username == null)}"*/}
- <a className="flex text-white text-base no-underline hover:text-white hover:no-underline" href="#">
- ☕<span className="hidden w-0 md:w-auto md:block pl-1">Spring!</span>
- </a>
-{/*th:if="${!(user == null || user.username == null)}"*/}
- <a className="flex text-white text-base no-underline hover:text-white hover:no-underline" href="#">
-{/*th:text="'Logged in as: ' + ${user.username}"*/}
- ☕<span className="hidden w-0 md:w-auto md:block pl-1"></span>
- </a>
+<a className="flex text-white text-base no-underline whitespace-nowrap hover:text-white hover:no-underline" href="#">☕ { user === null ? "Spring!" : user }<span className="hidden w-0 md:w-auto md:block pl-1"></span></a>
</div>
<div className="flex pl-4 text-sm place-content-between w-full">
<ul className="list-reset flex justify-between flex-1 md:flex-none items-center">
@@ -26,16 +44,14 @@ export default function Layout()
<a className="inline-block py-2 px-2 text-white no-underline" href="/">HOME</a>
</li>
<li className="mr-2">
- <a className="inline-block text-indigo-200 no-underline hover:text-gray-100 hover:text-underline py-2 px-2" href="/articles/new">NEW</a>
+ <a className="inline-block text-indigo-200 no-underline hover:text-gray-100 hover:text-underline py-2 px-2" href="/article/new">NEW</a>
</li>
<li className="mr-2">
<a className="inline-block text-indigo-200 no-underline hover:text-indigo-100 hover:text-underline py-2 px-2" href="/register">REGISTER</a>
</li>
- <li className="mr-2">
-{/*th:if="${(user == null || user.username == null)}"*/}
- <a className="inline-block text-indigo-200 no-underline hover:text-indigo-100 hover:text-underline py-2 px-2" href="/userlogin">LOGIN</a>
-{/*th:if="${!(user == null || user.username == null)}"*/}
- <a className="inline-block text-indigo-200 no-underline hover:text-indigo-100 hover:text-underline py-2 px-2" href="/logout">LOGOUT</a>
+ <li className="mr-2">{ user === null ?
+ <a className="inline-block text-indigo-200 no-underline hover:text-indigo-100 hover:text-underline py-2 px-2" href="/login">LOGIN</a> :
+ <button className="inline-block text-indigo-200 no-underline hover:text-indigo-100 hover:text-underline py-2 px-2" onClick={logout} >LOGOUT</button> }
</li>
</ul>
{/*th:action="@{/articles/search}"*/}
diff --git a/frontend/src/pages/Article.tsx b/frontend/src/pages/articles/Article.tsx
index b367811..b367811 100644
--- a/frontend/src/pages/Article.tsx
+++ b/frontend/src/pages/articles/Article.tsx
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<HTMLFormElement>) => {
+ 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(
+ <>
+<div className="flex justify-center bg-white p-12">
+<form onSubmit={handleSubmit} method="post" className="w-full max-w-lg">
+ <div className="flex flex-wrap -mx-3 mb-6">
+ <div className="w-full md:w-1/2 px-3 mb-6 md:mb-0">
+ <label className="block uppercase tracking-wide text-gray-700 text-xs font-bold mb-2">
+ Title
+ </label>
+ <input className="appearance-none block w-full bg-gray-200 text-gray-700 border border-red-500 rounded py-3 px-4 mb-3 leading-tight focus:outline-none focus:bg-white"
+ id="title"
+ type="text"
+ name="title"
+ placeholder="Yep"/>
+ <p className="text-red-500 text-xs italic">Please fill out this field.</p>
+ </div>
+ <div className="w-full md:w-1/2 px-3">
+ <label className="block uppercase tracking-wide text-gray-700 text-xs font-bold mb-2">
+ Photo URL
+ </label>
+ <input className="appearance-none block w-full bg-gray-200 text-gray-700 border border-gray-200 rounded py-3 px-4 leading-tight focus:outline-none focus:bg-white focus:border-gray-500"
+ id="photoUrl"
+ type="text"
+ name="photoUrl"
+ placeholder="Doe"/>
+ <p className="text-red-500 text-xs italic">Please fill out this field.</p>
+ </div>
+ </div>
+ <div className="flex flex-wrap -mx-3 mb-6">
+ <div className="w-full px-3">
+ <label className="block uppercase tracking-wide text-gray-700 text-xs font-bold mb-2">
+ Content
+ </label>
+ <input className="appearance-none block w-full bg-gray-200 text-gray-700 border border-gray-200 rounded py-3 px-4 leading-tight focus:outline-none focus:bg-white focus:border-gray-500"
+ id="content"
+ type="text"
+ name="content"
+ placeholder="Doe"/>
+ <p className="text-red-500 text-xs italic">Please fill out this field.</p>
+ </div>
+ </div>
+ <div className="flex flex-wrap mb-2">
+ </div>
+ <button type="submit" className="bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded">Create</button>
+
+</form>
+</div>
+ </>
+ );
+}
diff --git a/frontend/src/pages/Login.tsx b/frontend/src/pages/auth/Login.tsx
index 317fdb9..5a1e858 100644
--- a/frontend/src/pages/Login.tsx
+++ b/frontend/src/pages/auth/Login.tsx
@@ -1,7 +1,10 @@
-import { useState, useEffect } from "react";
-import { useParams, useNavigate } from "react-router-dom";
+import { FormEvent } from "react";
+import { useNavigate } from "react-router-dom";
-export default function Article () {
+//type setUser = { setUser: { func: React.Dispatch<React.SetStateAction<string | null>> } };
+type user = { set: React.Dispatch<React.SetStateAction<string | null>>, value: string | null };
+
+export default function Login ({user}: {user: user}) {
const navigate = useNavigate();
const handleSubmit = async (e: FormEvent<HTMLFormElement>) => {
@@ -17,24 +20,39 @@ const handleSubmit = async (e: FormEvent<HTMLFormElement>) => {
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`, {
+ 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");
+ }
});
- if(response.ok) {
- navigate("/");
- }
- else {
- alert("error");
- }
};
return(
<>
-<div className="text-xl p-4 bg-black text-red-500">Invalid Username/Password</div>
-<div className="text-xl p-4 bg-black text-red-500">You have been logged out</div>
-
<div className="flex h-full justify-center bg-white p-12">
<form onSubmit={handleSubmit} method="post" className="w-full max-w-lg">
<div className="flex flex-wrap -mx-3 mb-6">
diff --git a/frontend/src/pages/Register.tsx b/frontend/src/pages/auth/Register.tsx
index 14ceea4..14ceea4 100644
--- a/frontend/src/pages/Register.tsx
+++ b/frontend/src/pages/auth/Register.tsx
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<React.SetStateAction<string | null>>, value: string | null };
export default function Index()
{
+ const [user, setUser] = useState<string | null>(null);
+
+ const userProp: user = { set: setUser, value: user };
+
return (<>
<Router>
<Routes>
- <Route path="/" element = {<Layout/>}>
+ <Route path="/" element = {<Layout />}>
<Route index element={<Home />} />
<Route path="/article/:id" element={<Article />} />
+ <Route path="/article/new" element={<NewArticle />} />
<Route path="register" element={<Register />} />
- <Route path="login" element={<Login />} />
+ <Route path="login" element={<Login user={userProp}/>} />
</Route>
</Routes>
</Router>