mirror of
https://gitlab.com/etc404/software-engineering-project.git
synced 2026-05-10 20:52:58 +00:00
linked up create recipe html to api, also added some more validation to recipe inputs
This commit is contained in:
@@ -0,0 +1,34 @@
|
||||
package com.example.demo.controller;
|
||||
|
||||
import jakarta.validation.ConstraintViolationException;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.bind.MethodArgumentNotValidException;
|
||||
import org.springframework.web.bind.annotation.ExceptionHandler;
|
||||
import org.springframework.web.bind.annotation.RestControllerAdvice;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
@RestControllerAdvice
|
||||
public class GlobalValidationHandler {
|
||||
|
||||
@ExceptionHandler(ConstraintViolationException.class)
|
||||
public ResponseEntity<Map<String, String>> handleConstraintViolation(ConstraintViolationException ex) {
|
||||
Map<String, String> errors = new HashMap<>();
|
||||
ex.getConstraintViolations().forEach(violation -> {
|
||||
String fieldPath = violation.getPropertyPath().toString();
|
||||
errors.put(fieldPath, violation.getMessage());
|
||||
});
|
||||
return new ResponseEntity<>(errors, HttpStatus.BAD_REQUEST);
|
||||
}
|
||||
|
||||
@ExceptionHandler(MethodArgumentNotValidException.class)
|
||||
public ResponseEntity<Map<String, String>> handleValidationExceptions(MethodArgumentNotValidException ex) {
|
||||
Map<String, String> errors = new HashMap<>();
|
||||
ex.getBindingResult().getFieldErrors().forEach(error -> {
|
||||
errors.put(error.getField(), error.getDefaultMessage());
|
||||
});
|
||||
return new ResponseEntity<>(errors, HttpStatus.BAD_REQUEST);
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,8 @@
|
||||
package com.example.demo.controller;
|
||||
|
||||
import java.security.Principal;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
@@ -13,20 +15,26 @@ import org.springframework.web.bind.annotation.RequestBody;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RequestParam;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
import org.springframework.security.core.Authentication;
|
||||
import org.springframework.security.core.context.SecurityContextHolder;
|
||||
import org.springframework.security.core.userdetails.UserDetails;
|
||||
|
||||
import com.example.demo.dto.UserDto;
|
||||
import com.example.demo.entity.User;
|
||||
import com.example.demo.service.UserService;
|
||||
import com.example.demo.repository.UserRepo;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/api/users")
|
||||
public class UserController {
|
||||
|
||||
private UserService userService;
|
||||
private UserRepo userRepo;
|
||||
|
||||
public UserController(UserService userService) {
|
||||
public UserController(UserService userService, UserRepo userRepo) {
|
||||
super();
|
||||
this.userService = userService;
|
||||
this.userRepo = userRepo;
|
||||
}
|
||||
|
||||
// build create user REST API
|
||||
@@ -48,6 +56,16 @@ public class UserController {
|
||||
List<UserDto> users = userService.getUsersByName(string);
|
||||
return new ResponseEntity<>(users, HttpStatus.OK);
|
||||
}
|
||||
|
||||
// build get current user REST API
|
||||
@GetMapping("/me")
|
||||
public UserDto getLoggedInUser(Principal principal) {
|
||||
if (principal == null) return null;
|
||||
String username = principal.getName();
|
||||
User user = (userRepo.findByUsername(username))
|
||||
.orElse(null);
|
||||
return userService.convertToDto(user);
|
||||
}
|
||||
|
||||
|
||||
// build get user by id REST API
|
||||
|
||||
@@ -4,6 +4,8 @@ import jakarta.persistence.Entity;
|
||||
import jakarta.persistence.FetchType;
|
||||
import jakarta.persistence.Table;
|
||||
import jakarta.persistence.UniqueConstraint;
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
import jakarta.validation.constraints.Size;
|
||||
import jakarta.persistence.Id;
|
||||
import jakarta.persistence.OneToMany;
|
||||
import jakarta.persistence.GeneratedValue;
|
||||
@@ -23,7 +25,9 @@ public class Ingredient {
|
||||
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||
private Integer id;
|
||||
|
||||
@Column(nullable = false, unique = true)
|
||||
@Column(nullable = false, unique = true,columnDefinition = "TEXT")
|
||||
@NotBlank(message = "Please provide an ingredient name")
|
||||
@Size(max = 128, message = "Name cannot be longer than 128 characters")
|
||||
private String name;
|
||||
|
||||
@OneToMany(mappedBy = "ingredient")
|
||||
|
||||
@@ -5,6 +5,7 @@ import jakarta.validation.constraints.NotBlank;
|
||||
import jakarta.validation.constraints.NotEmpty;
|
||||
import jakarta.validation.constraints.NotNull;
|
||||
import jakarta.validation.constraints.Positive;
|
||||
import jakarta.validation.constraints.Size;
|
||||
import lombok.EqualsAndHashCode;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
@@ -19,18 +20,21 @@ public class Recipe {
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||
private Integer id;
|
||||
|
||||
|
||||
@Column(columnDefinition = "TEXT")
|
||||
@NotBlank(message = "Please provide a recipe title")
|
||||
@Size(max = 128, message = "Title cannot be longer than 128 characters")
|
||||
private String title;
|
||||
|
||||
@Column(columnDefinition = "TEXT")
|
||||
@Size(max = 500, message = "Description cannot be longer than 500 characters")
|
||||
private String description;
|
||||
|
||||
@NotNull(message = "Please Provide a prep time amount in mintutes")
|
||||
@NotNull(message = "Please Provide a prep time amount in minutes")
|
||||
@Positive(message = "This value cannot be negative")
|
||||
private Integer prepTimeMinutes;
|
||||
|
||||
@NotNull(message = "Please Provide a cook time amount in mintutes")
|
||||
@NotNull(message = "Please Provide a cook time amount in minutes")
|
||||
@Positive(message = "This value cannot be negative")
|
||||
private Integer cookTimeMinutes;
|
||||
|
||||
@@ -51,11 +55,14 @@ public class Recipe {
|
||||
|
||||
// Recipe ingredients relationship
|
||||
@OneToMany(mappedBy = "recipe", cascade = CascadeType.ALL, orphanRemoval = true, fetch = FetchType.LAZY)
|
||||
@NotEmpty(message = "At least one ingredient is required")
|
||||
@NotEmpty(message = "At least one ingredient is required")
|
||||
@Size(max = 256, message = "Cannot have more than 256 ingredients")
|
||||
private Set<RecipeIngredient> recipeIngredients = new HashSet<>();
|
||||
|
||||
// Recipe Steps relationship
|
||||
@OneToMany(mappedBy = "recipe", cascade = CascadeType.ALL, orphanRemoval = true, fetch = FetchType.LAZY)
|
||||
@NotEmpty(message = "At least one step is required")
|
||||
@Size(max = 50, message = "Cannot have more than 50 steps")
|
||||
private Set<Step> steps = new HashSet<>();
|
||||
|
||||
// Recipe Images relationship
|
||||
|
||||
@@ -1,6 +1,10 @@
|
||||
package com.example.demo.entity;
|
||||
|
||||
import jakarta.persistence.*;
|
||||
import jakarta.validation.constraints.NotNull;
|
||||
import jakarta.validation.constraints.Positive;
|
||||
import jakarta.validation.constraints.Size;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import lombok.EqualsAndHashCode;
|
||||
|
||||
@@ -23,11 +27,17 @@ public class RecipeIngredient {
|
||||
@JoinColumn(name = "ingredient_id", nullable = false)
|
||||
@EqualsAndHashCode.Include
|
||||
private Ingredient ingredient;
|
||||
|
||||
|
||||
@NotNull(message = "Please Provide a Quantity")
|
||||
@Positive(message = "Quantity cannot be negative")
|
||||
private BigDecimal quantity;
|
||||
|
||||
|
||||
@Column(columnDefinition = "TEXT")
|
||||
@Size(max = 32, message = "Unit cannot be longer than 32 characters")
|
||||
private String unit;
|
||||
|
||||
|
||||
@Column(columnDefinition = "TEXT")
|
||||
@Size(max = 128, message = "Note cannot be longer than 128 characters")
|
||||
private String notes;
|
||||
|
||||
public RecipeIngredient() {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package com.example.demo.entity;
|
||||
|
||||
import jakarta.persistence.*;
|
||||
import jakarta.validation.constraints.Size;
|
||||
import lombok.EqualsAndHashCode;
|
||||
|
||||
@Entity
|
||||
@@ -15,6 +16,7 @@ public class Step {
|
||||
private Integer stepNumber;
|
||||
|
||||
@Column(name = "instruction", nullable = false, columnDefinition = "TEXT")
|
||||
@Size(max = 500, message = "Instruction cannot be longer than 500 characters")
|
||||
private String instruction;
|
||||
|
||||
@ManyToOne(fetch = FetchType.LAZY)
|
||||
|
||||
@@ -16,6 +16,8 @@ import jakarta.persistence.CascadeType;
|
||||
import jakarta.persistence.Column;
|
||||
import org.springframework.security.core.GrantedAuthority;
|
||||
import org.springframework.security.core.userdetails.UserDetails;
|
||||
import com.fasterxml.jackson.annotation.JsonIgnore;
|
||||
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.ArrayList;
|
||||
@@ -53,6 +55,7 @@ public class User implements UserDetails {
|
||||
|
||||
// Favorite relationship and also junction table
|
||||
@ManyToMany(fetch = FetchType.LAZY)
|
||||
@JsonIgnore
|
||||
@JoinTable(name = "favorites", joinColumns = { @JoinColumn(name = "userId") }, inverseJoinColumns = {
|
||||
@JoinColumn(name = "recipeId") })
|
||||
private Set<Recipe> FavRecipes = new HashSet<>();
|
||||
|
||||
@@ -1,101 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>Create Thyme Crunch Account</title>
|
||||
<link rel="stylesheet" th:href="@{css/create-account.css}">
|
||||
<link href="https://fonts.googleapis.com/css2?family=Delius+Swash+Caps&family=Mali:ital,wght@0,200;0,300;0,400;0,500;0,600;0,700;1,200;1,300;1,400;1,500;1,600;1,700" rel="stylesheet">
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<header class="top-header">
|
||||
<img th:src="@{images/header_left.png}" alt="Violin f-hole shape to the left of header." class="swirl">
|
||||
<h1 class="site-name">Thyme Crunch</h1>
|
||||
<img th:src="@{images/header_right.png}" alt="Violin f-hole shape to the right of header." class="swirl">
|
||||
</header>
|
||||
|
||||
<!-- Main Content -->
|
||||
<main class="main-content">
|
||||
<div class="login-box">
|
||||
<h2>Create Account</h2>
|
||||
|
||||
<form id="createUserForm">
|
||||
<div class="rows">
|
||||
<label for="username">Username</label>
|
||||
<input type="text" id="username" required>
|
||||
</div>
|
||||
<div class="rows">
|
||||
<label for="email">Email</label>
|
||||
<input type="email" id="email" required>
|
||||
</div>
|
||||
<div class="rows">
|
||||
<label for="password">Password</label>
|
||||
<input type="password" id="password" required>
|
||||
</div>
|
||||
<div class="rows">
|
||||
<label for="confirmPassword">Confirm Password</label>
|
||||
<input type="password" id="confirmPassword" required>
|
||||
</div>
|
||||
<p id="passwordError"></p>
|
||||
|
||||
<button type="submit">Create</button>
|
||||
</form>
|
||||
</div>
|
||||
</main>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
<script>
|
||||
document.addEventListener("DOMContentLoaded", function () {
|
||||
|
||||
const passwordField = document.getElementById("password");
|
||||
const confirmPasswordField = document.getElementById("confirmPassword");
|
||||
|
||||
function checkPasswords() {
|
||||
|
||||
if (confirmPasswordField.value === "") {
|
||||
confirmPasswordField.classList.remove("invalid");
|
||||
return;
|
||||
}
|
||||
|
||||
if (passwordField.value !== confirmPasswordField.value) {
|
||||
confirmPasswordField.classList.add("invalid");
|
||||
} else {
|
||||
confirmPasswordField.classList.remove("invalid");
|
||||
}
|
||||
}
|
||||
|
||||
passwordField.addEventListener("input", checkPasswords);
|
||||
confirmPasswordField.addEventListener("input", checkPasswords);
|
||||
|
||||
document.getElementById("createUserForm").addEventListener("submit", function(e) {
|
||||
|
||||
const password = passwordField.value;
|
||||
const confirmPassword = confirmPasswordField.value;
|
||||
|
||||
if (password !== confirmPassword) {
|
||||
e.preventDefault();
|
||||
confirmPasswordField.classList.add("invalid");
|
||||
return;
|
||||
}
|
||||
|
||||
const userData = {
|
||||
username: document.getElementById("username").value,
|
||||
email: document.getElementById("email").value,
|
||||
hashedpassword: password,
|
||||
role: "USER"
|
||||
};
|
||||
|
||||
fetch("http://localhost:8080/api/users", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json"
|
||||
},
|
||||
body: JSON.stringify(userData)
|
||||
});
|
||||
|
||||
e.preventDefault();
|
||||
});
|
||||
|
||||
});
|
||||
</script>
|
||||
@@ -2,6 +2,8 @@
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="_csrf_header" th:content="${_csrf.headerName}"/>
|
||||
<meta name="_csrf" th:content="${_csrf.token}"/>
|
||||
<title>Create Thyme Crunch Recipe</title>
|
||||
<link rel="stylesheet" th:href="@{css/create-recipe.css}">
|
||||
<link href="https://fonts.googleapis.com/css2?family=Delius+Swash+Caps&family=Mali:ital,wght@0,200;0,300;0,400;0,500;0,600;0,700;1,200;1,300;1,400;1,500;1,600;1,700" rel="stylesheet">
|
||||
@@ -226,6 +228,117 @@ function renderTags() {
|
||||
btn.addEventListener('click', () => removeTag(btn.dataset.tag));
|
||||
});
|
||||
}
|
||||
|
||||
async function getLoggedInUser() {
|
||||
try {
|
||||
const res = await fetch('http://localhost:8080/api/users/me', {
|
||||
credentials: 'include'
|
||||
});
|
||||
if (!res.ok) throw new Error('Failed to get logged-in user');
|
||||
return await res.json();
|
||||
} catch (err) {
|
||||
console.error('Error fetching user:', err);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function buildRecipeJSON(user) {
|
||||
const title = document.getElementById('title').value.trim();
|
||||
const description = document.getElementById('desc').value.trim();
|
||||
const prepTimeMinutes = Number(document.getElementById('prep').value);
|
||||
const cookTimeMinutes = Number(document.getElementById('cooking').value);
|
||||
const servings = Number(document.getElementById('servings').value);
|
||||
const status = "DRAFT";
|
||||
|
||||
// Ingredients
|
||||
const recipeIngredients = [...document.querySelectorAll('#ingredients-container .dynamic-row')]
|
||||
.map(row => {
|
||||
const qtyValue = Number(row.querySelector('.ing-qty').value.trim());
|
||||
return {
|
||||
ingredient: { name: row.querySelector('.ing-name').value.trim() },
|
||||
quantity: qtyValue,
|
||||
unit: row.querySelector('.ing-unit').value.trim(),
|
||||
notes: row.querySelector('.ing-notes').value.trim()
|
||||
};
|
||||
})
|
||||
.filter(item => item.ingredient.name);
|
||||
|
||||
// Steps
|
||||
const steps = [...document.querySelectorAll('#steps-container textarea')]
|
||||
.map((el, i) => ({ stepNumber: i + 1, instruction: el.value.trim() }))
|
||||
.filter(item => item.instruction);
|
||||
|
||||
// Images
|
||||
|
||||
|
||||
// Tags
|
||||
const tagsInput = tags;
|
||||
const tagsArray = tagsInput.map(t => ({ name: t }));
|
||||
|
||||
return {
|
||||
title,
|
||||
description,
|
||||
prepTimeMinutes,
|
||||
cookTimeMinutes,
|
||||
servings,
|
||||
status,
|
||||
user,
|
||||
recipeIngredients,
|
||||
steps,
|
||||
//images,
|
||||
tags: tagsArray
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
document.getElementById('publish-btn').addEventListener('click', async function(e) {
|
||||
e.preventDefault();
|
||||
|
||||
const user = await getLoggedInUser();
|
||||
if (!user) {
|
||||
alert("Unable to fetch logged-in user. Please refresh and try again.");
|
||||
return;
|
||||
}
|
||||
|
||||
console.log("Logged-in user:", user);
|
||||
const recipeJSON = buildRecipeJSON(user);
|
||||
console.log("Recipe JSON to submit:", JSON.stringify(recipeJSON, null, 2));
|
||||
|
||||
try {
|
||||
|
||||
const csrfToken = document.querySelector('meta[name="_csrf"]').getAttribute('content');
|
||||
const csrfHeader = document.querySelector('meta[name="_csrf_header"]').getAttribute('content');
|
||||
|
||||
|
||||
|
||||
const res = await fetch('http://localhost:8080/api/recipes', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
[csrfHeader]: csrfToken,
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify(recipeJSON),
|
||||
credentials: 'include'
|
||||
});
|
||||
|
||||
if (res.ok) {
|
||||
const data = await res.json();
|
||||
console.log("Recipe created:", data);
|
||||
alert("Recipe created successfully!");
|
||||
} else {
|
||||
const errorData = await res.json();
|
||||
console.error("Validation errors:", errorData);
|
||||
|
||||
|
||||
const firstError = Object.values(errorData)[0];
|
||||
alert(firstError);
|
||||
}
|
||||
} catch (err) {
|
||||
console.error("Network error:", err);
|
||||
alert("Network error. Check console for details.");
|
||||
}
|
||||
});
|
||||
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
Reference in New Issue
Block a user