From 634f31c390da1c3b055aa4a62c98239e27e12de5 Mon Sep 17 00:00:00 2001 From: james Date: Mon, 6 Apr 2026 10:09:52 -0400 Subject: [PATCH 01/52] Whitespace --- .../com/zipcode/stardust/repository/UserRepository.java | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/java/src/main/java/com/zipcode/stardust/repository/UserRepository.java b/java/src/main/java/com/zipcode/stardust/repository/UserRepository.java index 9a98b06..48be50f 100644 --- a/java/src/main/java/com/zipcode/stardust/repository/UserRepository.java +++ b/java/src/main/java/com/zipcode/stardust/repository/UserRepository.java @@ -1,9 +1,11 @@ package com.zipcode.stardust.repository; -import com.zipcode.stardust.model.User; -import org.springframework.data.jpa.repository.JpaRepository; import java.util.Optional; +import org.springframework.data.jpa.repository.JpaRepository; + +import com.zipcode.stardust.model.User; + public interface UserRepository extends JpaRepository { Optional findByUsername(String username); Optional findByEmail(String email); From fa0ebf1b7409f08c6e376958b97866649ce7edc6 Mon Sep 17 00:00:00 2001 From: james Date: Mon, 6 Apr 2026 10:30:33 -0400 Subject: [PATCH 02/52] Renamed test files --- .../StardustApplicationTests.java} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename java/src/test/java/com/zipcode/{circuscircus/CircusCircusApplicationTests.java => stardust/StardustApplicationTests.java} (100%) diff --git a/java/src/test/java/com/zipcode/circuscircus/CircusCircusApplicationTests.java b/java/src/test/java/com/zipcode/stardust/StardustApplicationTests.java similarity index 100% rename from java/src/test/java/com/zipcode/circuscircus/CircusCircusApplicationTests.java rename to java/src/test/java/com/zipcode/stardust/StardustApplicationTests.java From 635d38bbe63d02b85bcb372c13f374fe25add741 Mon Sep 17 00:00:00 2001 From: mahala Date: Mon, 6 Apr 2026 11:35:26 -0400 Subject: [PATCH 03/52] Changed Name --- .../{circuscircus => stardust}/CircusCircusApplicationTests.java | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename java/src/test/java/com/zipcode/{circuscircus => stardust}/CircusCircusApplicationTests.java (100%) diff --git a/java/src/test/java/com/zipcode/circuscircus/CircusCircusApplicationTests.java b/java/src/test/java/com/zipcode/stardust/CircusCircusApplicationTests.java similarity index 100% rename from java/src/test/java/com/zipcode/circuscircus/CircusCircusApplicationTests.java rename to java/src/test/java/com/zipcode/stardust/CircusCircusApplicationTests.java From 78290e8b0e0b6bf383389ec0559b76f702359384 Mon Sep 17 00:00:00 2001 From: james Date: Mon, 6 Apr 2026 15:21:10 -0400 Subject: [PATCH 04/52] Branch and Port fixes --- java/pom.xml | 2 -- java/src/main/resources/application.properties | 2 +- java/src/test/resources/application.properties | 2 +- 3 files changed, 2 insertions(+), 4 deletions(-) diff --git a/java/pom.xml b/java/pom.xml index d35d08a..941dfdf 100644 --- a/java/pom.xml +++ b/java/pom.xml @@ -45,12 +45,10 @@ org.xerial sqlite-jdbc - 3.45.3.0 org.hibernate.orm hibernate-community-dialects - 6.4.4.Final org.springframework.boot diff --git a/java/src/main/resources/application.properties b/java/src/main/resources/application.properties index 3a5f22b..ebd39f7 100644 --- a/java/src/main/resources/application.properties +++ b/java/src/main/resources/application.properties @@ -3,7 +3,7 @@ spring.datasource.driver-class-name=org.sqlite.JDBC spring.jpa.database-platform=org.hibernate.community.dialect.SQLiteDialect spring.jpa.hibernate.ddl-auto=update spring.jpa.show-sql=false -server.port=5000 +server.port=8080 spring.application.name=Schooner site.name=Schooner site.description=a schooner forum diff --git a/java/src/test/resources/application.properties b/java/src/test/resources/application.properties index a1e18e6..601f780 100644 --- a/java/src/test/resources/application.properties +++ b/java/src/test/resources/application.properties @@ -3,7 +3,7 @@ spring.datasource.driver-class-name=org.sqlite.JDBC spring.jpa.database-platform=org.hibernate.community.dialect.SQLiteDialect spring.jpa.hibernate.ddl-auto=create-drop spring.jpa.show-sql=false -server.port=5000 +server.port=8080 spring.application.name=Schooner site.name=Schooner site.description=a schooner forum From 9cd019e4c6aa37879e05721c8451b1a022865d34 Mon Sep 17 00:00:00 2001 From: mahala Date: Mon, 6 Apr 2026 16:53:30 -0400 Subject: [PATCH 05/52] Changed Server port --- java/src/main/resources/application.properties | 2 +- java/src/test/resources/application.properties | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/java/src/main/resources/application.properties b/java/src/main/resources/application.properties index 3a5f22b..ebd39f7 100644 --- a/java/src/main/resources/application.properties +++ b/java/src/main/resources/application.properties @@ -3,7 +3,7 @@ spring.datasource.driver-class-name=org.sqlite.JDBC spring.jpa.database-platform=org.hibernate.community.dialect.SQLiteDialect spring.jpa.hibernate.ddl-auto=update spring.jpa.show-sql=false -server.port=5000 +server.port=8080 spring.application.name=Schooner site.name=Schooner site.description=a schooner forum diff --git a/java/src/test/resources/application.properties b/java/src/test/resources/application.properties index a1e18e6..601f780 100644 --- a/java/src/test/resources/application.properties +++ b/java/src/test/resources/application.properties @@ -3,7 +3,7 @@ spring.datasource.driver-class-name=org.sqlite.JDBC spring.jpa.database-platform=org.hibernate.community.dialect.SQLiteDialect spring.jpa.hibernate.ddl-auto=create-drop spring.jpa.show-sql=false -server.port=5000 +server.port=8080 spring.application.name=Schooner site.name=Schooner site.description=a schooner forum From a7ab228190b570fec1d4f32f3310d8c33d9faace Mon Sep 17 00:00:00 2001 From: james Date: Tue, 7 Apr 2026 21:13:34 -0400 Subject: [PATCH 06/52] Markdown Support --- java/pom.xml | 10 ++++++++ .../stardust/controller/ForumController.java | 8 ++++++ .../stardust/service/ForumService.java | 25 +++++++++++++++++++ .../main/resources/templates/createpost.html | 4 +++ .../main/resources/templates/viewpost.html | 4 +-- 5 files changed, 49 insertions(+), 2 deletions(-) diff --git a/java/pom.xml b/java/pom.xml index 941dfdf..4cc7490 100644 --- a/java/pom.xml +++ b/java/pom.xml @@ -60,6 +60,16 @@ spring-security-test test + + org.commonmark + commonmark + 0.22.0 + + + com.googlecode.owasp-java-html-sanitizer + owasp-java-html-sanitizer + 20240325.1 + diff --git a/java/src/main/java/com/zipcode/stardust/controller/ForumController.java b/java/src/main/java/com/zipcode/stardust/controller/ForumController.java index 73f2019..61a0d85 100644 --- a/java/src/main/java/com/zipcode/stardust/controller/ForumController.java +++ b/java/src/main/java/com/zipcode/stardust/controller/ForumController.java @@ -12,7 +12,9 @@ import org.springframework.web.bind.annotation.*; import java.util.ArrayList; +import java.util.LinkedHashMap; import java.util.List; +import java.util.Map; import java.util.Optional; @Controller @@ -170,8 +172,14 @@ public String viewPost(@RequestParam Long post, Model model, Authentication auth Post p = opt.get(); List comments = commentRepository.findByPostOrderByPostdateAsc(p); String breadcrumb = forumService.generateLinkPath(p.getSubforum().getId()); + Map commentContents = new LinkedHashMap<>(); + for (Comment c : comments) { + commentContents.put(c.getId(), forumService.renderMarkdown(c.getContent())); + } model.addAttribute("post", p); + model.addAttribute("postContent", forumService.renderMarkdown(p.getContent())); model.addAttribute("comments", comments); + model.addAttribute("commentContents", commentContents); model.addAttribute("breadcrumb", breadcrumb); model.addAttribute("errors", new ArrayList<>()); return "viewpost"; diff --git a/java/src/main/java/com/zipcode/stardust/service/ForumService.java b/java/src/main/java/com/zipcode/stardust/service/ForumService.java index 1ebbc72..0b117cb 100644 --- a/java/src/main/java/com/zipcode/stardust/service/ForumService.java +++ b/java/src/main/java/com/zipcode/stardust/service/ForumService.java @@ -3,6 +3,11 @@ import com.zipcode.stardust.model.Subforum; import com.zipcode.stardust.repository.SubforumRepository; import com.zipcode.stardust.repository.UserRepository; +import org.commonmark.node.Node; +import org.commonmark.parser.Parser; +import org.commonmark.renderer.html.HtmlRenderer; +import org.owasp.html.HtmlPolicyBuilder; +import org.owasp.html.PolicyFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import org.springframework.web.util.HtmlUtils; @@ -18,6 +23,26 @@ public class ForumService { @Autowired private UserRepository userRepository; + // Markdown rendering pipeline — thread-safe singletons + private final Parser mdParser = Parser.builder().build(); + private final HtmlRenderer mdRenderer = HtmlRenderer.builder().build(); + private final PolicyFactory sanitizer = new HtmlPolicyBuilder() + .allowElements("p", "br", "hr", "b", "strong", "em", "i", "u", "s", + "code", "pre", "blockquote", "ul", "ol", "li", + "h1", "h2", "h3", "h4") + .allowUrlProtocols("http", "https") + .allowElements("a") + .allowAttributes("href").onElements("a") + .requireRelNofollowOnLinks() + .toFactory(); + + public String renderMarkdown(String raw) { + if (raw == null) return ""; + Node document = mdParser.parse(raw); + String html = mdRenderer.render(document); + return sanitizer.sanitize(html); + } + public String generateLinkPath(Long subforumId) { StringBuilder sb = new StringBuilder(); sb.append(" / Forum Index"); diff --git a/java/src/main/resources/templates/createpost.html b/java/src/main/resources/templates/createpost.html index ee04f65..8854db8 100644 --- a/java/src/main/resources/templates/createpost.html +++ b/java/src/main/resources/templates/createpost.html @@ -21,6 +21,10 @@

Create Post in Forum

+ + Markdown supported: **bold**, _italic_, + `code`, # Heading, - list item + Cancel diff --git a/java/src/main/resources/templates/viewpost.html b/java/src/main/resources/templates/viewpost.html index 6762abc..fbce6f9 100644 --- a/java/src/main/resources/templates/viewpost.html +++ b/java/src/main/resources/templates/viewpost.html @@ -16,11 +16,11 @@

Post Title

time

-
Content here
+
Content here
Comments (0)
-

Comment content

+

Comment content

by authortime From cf48dfddace5027d79678e0ae77293e3ea8b3d95 Mon Sep 17 00:00:00 2001 From: james Date: Wed, 8 Apr 2026 13:23:35 -0400 Subject: [PATCH 07/52] Message file and Message controller file --- Stardust.db | Bin 0 -> 32768 bytes .../controller/MessageController.java | 179 ++++++++++++++++++ .../com/zipcode/stardust/model/Message.java | 99 ++++++++++ .../repository/MessageRepository.java | 16 ++ .../service/CommonAttributesHelper.java | 46 +++++ 5 files changed, 340 insertions(+) create mode 100644 Stardust.db create mode 100644 java/src/main/java/com/zipcode/stardust/controller/MessageController.java create mode 100644 java/src/main/java/com/zipcode/stardust/model/Message.java create mode 100644 java/src/main/java/com/zipcode/stardust/repository/MessageRepository.java create mode 100644 java/src/main/java/com/zipcode/stardust/service/CommonAttributesHelper.java diff --git a/Stardust.db b/Stardust.db new file mode 100644 index 0000000000000000000000000000000000000000..48dfa9673dac572c860d82b1fcf6d830aca36752 GIT binary patch literal 32768 zcmeI)&u-H&90zb`>0d+}IE)_9BnyzXi2@yH7dVVD#tuxH*nkU%%56+*k=R}AFm~+V z$e9=56*zI=Dc})!1|&|{*|M!rhLHF>eIsS@9LM(WW2=(0dK>pvh2iwEN@8Z{HFAo0 z9=S;=A*5>0iaqnAU=!}l4=+y~ajlZe@@=jDf|O4tBv`M%ti7sloJ?wQ?e-B*;T8lS z009U<00Izz00ba#Oaz8&#d2eE(tGY1)`@tkdE(B}()#k^{bhQ8@z&}xJ?FB{(HRj^ zAr0^Gq)AyA3rRalMVv`0m7y|?q9)}r6A^vNl5U?RGgs&5XZLT?REo`%H|c?61?ODB^6vDU=#ZQ{Usrd)uDNQ$;(mvvuZhX(F2$J0gpE zptNzPKN79W8I827>#0uX=z1Rwwb2tWV=5P-n35^(GPV*Nb{UI&}O zLjAK%;(!1IAOHafKmY;|fB*y_009X67XlZGC9+UyklR9cQ?02oeV$O$7qX|RP4YFD zu~aCLZe@-<&~|4)U+DdB`%|N`Jp+`vTCoH0S72B;9Ex$zWR?b(~ z`9LLxc2e8ImN0!=W45V2jk5-Cr9HY^M7JhNWTi4)eJJ>r+jehDFez0kyWH&)*zMrr z;haZanG7%5ct&!Yt}&Wq?P|L@MuxPv*S&BzZnyJ;byR8&b*953l`GtFji0b1S`6Nh z;A8M1_!PXeNgNP>00bZa0SG_<0uX=z1Rwwb2>b&ARlnh7uP};d{5kL2mlB1*KkprS z<=~(8r@fJv2NM;)QO(x>`2PPtFrerH1Rwwb2tWV=5P$##AOHafK;SnExWE77|Ns4F zWK<6U2tWV=5P$##AOHafKmY;|_{#!V|NrFyK=TlQ00bZa0SG_<0uX=z1Rwx`KOpc0 Dju6H} literal 0 HcmV?d00001 diff --git a/java/src/main/java/com/zipcode/stardust/controller/MessageController.java b/java/src/main/java/com/zipcode/stardust/controller/MessageController.java new file mode 100644 index 0000000..f987f41 --- /dev/null +++ b/java/src/main/java/com/zipcode/stardust/controller/MessageController.java @@ -0,0 +1,179 @@ +package com.zipcode.stardust.controller; + +import com.zipcode.stardust.model.Message; +import com.zipcode.stardust.model.User; +import com.zipcode.stardust.repository.MessageRepository; +import com.zipcode.stardust.repository.UserRepository; +import com.zipcode.stardust.service.CommonAttributesHelper; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.security.core.Authentication; +import org.springframework.stereotype.Controller; +import org.springframework.ui.Model; +import org.springframework.web.bind.annotation.*; + +import java.net.URLEncoder; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; + +@Controller +public class MessageController { + + @Autowired private MessageRepository messageRepository; + @Autowired private UserRepository userRepository; + @Autowired private CommonAttributesHelper helper; + + // ── Inbox ──────────────────────────────────────────────────────────────── + + @GetMapping("/messages/inbox") + public String inbox(Model model, Authentication auth) { + helper.addCommonAttributes(model, auth); + User user = helper.getCurrentUser(auth); + if (user == null) return "redirect:/loginform"; + + List messages = + messageRepository.findByRecipientAndDeletedByRecipientFalseOrderBySentAtDesc(user); + model.addAttribute("messages", messages); + return "messages/inbox"; + } + + // ── Outbox ─────────────────────────────────────────────────────────────── + + @GetMapping("/messages/outbox") + public String outbox(Model model, Authentication auth) { + helper.addCommonAttributes(model, auth); + User user = helper.getCurrentUser(auth); + if (user == null) return "redirect:/loginform"; + + List messages = + messageRepository.findBySenderAndDeletedBySenderFalseOrderBySentAtDesc(user); + model.addAttribute("messages", messages); + return "messages/outbox"; + } + + // ── Compose — show form ────────────────────────────────────────────────── + + @GetMapping("/messages/compose") + public String composeForm( + @RequestParam(required = false, defaultValue = "") String to, + @RequestParam(required = false, defaultValue = "") String subject, + Model model, Authentication auth) { + + helper.addCommonAttributes(model, auth); + if (helper.getCurrentUser(auth) == null) return "redirect:/loginform"; + + // Truncate pre-filled subject to 200 chars to stay within the field limit + if (subject.length() > 200) subject = subject.substring(0, 200); + + model.addAttribute("toUsername", to); + model.addAttribute("subjectPrefill", subject); + model.addAttribute("errors", new ArrayList<>()); + return "messages/compose"; + } + + // ── Compose — send message ─────────────────────────────────────────────── + + @PostMapping("/messages/compose") + public String sendMessage( + @RequestParam String to, + @RequestParam String subject, + @RequestParam String content, + Model model, Authentication auth) { + + helper.addCommonAttributes(model, auth); + User sender = helper.getCurrentUser(auth); + if (sender == null) return "redirect:/loginform"; + + List errors = new ArrayList<>(); + + // Validate subject and content + if (subject == null || subject.isBlank() || subject.length() > 200) + errors.add("Subject must be between 1 and 200 characters."); + if (content == null || content.isBlank() || content.length() > 4999) + errors.add("Message must be between 1 and 4999 characters."); + + // Look up the recipient by username + Optional recipientOpt = userRepository.findByUsername(to); + if (recipientOpt.isEmpty()) + errors.add("No user with the username \"" + to + "\" exists."); + + // If any errors, re-render the compose form with the values intact + if (!errors.isEmpty()) { + model.addAttribute("toUsername", to); + model.addAttribute("subjectPrefill", subject); + model.addAttribute("errors", errors); + return "messages/compose"; + } + + Message message = new Message(sender, recipientOpt.get(), subject, content); + messageRepository.save(message); + return "redirect:/messages/outbox"; + } + + // ── View a message ─────────────────────────────────────────────────────── + + @GetMapping("/messages/view") + public String viewMessage( + @RequestParam Long id, + Model model, Authentication auth) { + + helper.addCommonAttributes(model, auth); + User user = helper.getCurrentUser(auth); + if (user == null) return "redirect:/loginform"; + + Optional opt = messageRepository.findById(id); + if (opt.isEmpty()) return "redirect:/messages/inbox"; + Message message = opt.get(); + + // Security check — only the sender or recipient may view this message + boolean isSender = message.getSender().getId().equals(user.getId()); + boolean isRecipient = message.getRecipient().getId().equals(user.getId()); + if (!isSender && !isRecipient) return "redirect:/"; + + // Mark as read when the recipient opens it for the first time + if (isRecipient && !message.isRead()) { + message.setRead(true); + messageRepository.save(message); + } + + // Build the reply URL with a properly encoded subject line + String encodedSubject = URLEncoder.encode("Re: " + message.getSubject(), StandardCharsets.UTF_8); + String replyUrl = "/messages/compose?to=" + message.getSender().getUsername() + + "&subject=" + encodedSubject; + + model.addAttribute("message", message); + model.addAttribute("replyUrl", replyUrl); + model.addAttribute("isSender", isSender); + return "messages/view"; + } + + // ── Delete (soft) ──────────────────────────────────────────────────────── + + @PostMapping("/messages/delete") + public String deleteMessage( + @RequestParam Long id, + @RequestParam String box, + Authentication auth) { + + User user = helper.getCurrentUser(auth); + if (user == null) return "redirect:/loginform"; + + Optional opt = messageRepository.findById(id); + if (opt.isEmpty()) return "redirect:/messages/inbox"; + Message message = opt.get(); + + if ("outbox".equals(box) && message.getSender().getId().equals(user.getId())) { + message.setDeletedBySender(true); + messageRepository.save(message); + return "redirect:/messages/outbox"; + } + + if ("inbox".equals(box) && message.getRecipient().getId().equals(user.getId())) { + message.setDeletedByRecipient(true); + messageRepository.save(message); + } + + return "redirect:/messages/inbox"; + } +} diff --git a/java/src/main/java/com/zipcode/stardust/model/Message.java b/java/src/main/java/com/zipcode/stardust/model/Message.java new file mode 100644 index 0000000..f5f9d85 --- /dev/null +++ b/java/src/main/java/com/zipcode/stardust/model/Message.java @@ -0,0 +1,99 @@ +package com.zipcode.stardust.model; + +import java.time.Duration; +import java.time.LocalDateTime; + +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.FetchType; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.ManyToOne; +import jakarta.persistence.Table; + +@Entity +@Table(name = "message") +public class Message { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "sender_id") + private User sender; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "recipient_id") + private User recipient; + + @Column(nullable = false, length = 200) + private String subject; + + @Column(nullable = false, length = 5000) + private String content; + + @Column(nullable = false) + private LocalDateTime sentAt; + + @Column(nullable = false) + private boolean read = false; + + @Column(nullable = false) + private boolean deletedBySender = false; + + @Column(nullable = false) + private boolean deletedByRecipient = false; + + public Message() {} + + public Message(User sender, User recipient, String subject, String content) { + this.sender = sender; + this.recipient = recipient; + this.subject = subject; + this.content = content; + this.sentAt = LocalDateTime.now(); + } + + public String getTimeString() { + Duration d = Duration.between(sentAt, LocalDateTime.now()); + long months = d.toDays() / 30; + long days = d.toDays(); + long hours = d.toHours(); + long minutes = d.toMinutes(); + if (months > 0) return months + " month" + (months == 1 ? "" : "s") + " ago"; + if (days > 0) return days + " day" + (days == 1 ? "" : "s") + " ago"; + if (hours > 0) return hours + " hour" + (hours == 1 ? "" : "s") + " ago"; + if (minutes > 0) return minutes + " minute" + (minutes == 1 ? "" : "s") + " ago"; + return "Just now"; + } + + public Long getId() { return id; } + public void setId(Long id) { this.id = id; } + + public User getSender() { return sender; } + public void setSender(User sender) { this.sender = sender; } + + public User getRecipient() { return recipient; } + public void setRecipient(User recipient) { this.recipient = recipient; } + + public String getSubject() { return subject; } + public void setSubject(String subject) { this.subject = subject; } + + public String getContent() { return content; } + public void setContent(String content) { this.content = content; } + + public LocalDateTime getSentAt() { return sentAt; } + public void setSentAt(LocalDateTime sentAt) { this.sentAt = sentAt; } + + public boolean isRead() { return read; } + public void setRead(boolean read) { this.read = read; } + + public boolean isDeletedBySender() { return deletedBySender; } + public void setDeletedBySender(boolean v) { this.deletedBySender = v; } + + public boolean isDeletedByRecipient() { return deletedByRecipient; } + public void setDeletedByRecipient(boolean v) { this.deletedByRecipient = v; } +} diff --git a/java/src/main/java/com/zipcode/stardust/repository/MessageRepository.java b/java/src/main/java/com/zipcode/stardust/repository/MessageRepository.java new file mode 100644 index 0000000..29ae5ae --- /dev/null +++ b/java/src/main/java/com/zipcode/stardust/repository/MessageRepository.java @@ -0,0 +1,16 @@ +package com.zipcode.stardust.repository; + +import com.zipcode.stardust.model.Message; +import com.zipcode.stardust.model.User; +import org.springframework.data.jpa.repository.JpaRepository; + +import java.util.List; + +public interface MessageRepository extends JpaRepository { + + List findByRecipientAndDeletedByRecipientFalseOrderBySentAtDesc(User recipient); + + List findBySenderAndDeletedBySenderFalseOrderBySentAtDesc(User sender); + + long countByRecipientAndReadFalseAndDeletedByRecipientFalse(User recipient); +} diff --git a/java/src/main/java/com/zipcode/stardust/service/CommonAttributesHelper.java b/java/src/main/java/com/zipcode/stardust/service/CommonAttributesHelper.java new file mode 100644 index 0000000..d92bc56 --- /dev/null +++ b/java/src/main/java/com/zipcode/stardust/service/CommonAttributesHelper.java @@ -0,0 +1,46 @@ +package com.zipcode.stardust.service; + +import com.zipcode.stardust.model.User; +import com.zipcode.stardust.repository.MessageRepository; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.security.core.Authentication; +import org.springframework.stereotype.Component; +import org.springframework.ui.Model; + +@Component +public class CommonAttributesHelper { + + @Autowired + private MessageRepository messageRepository; + + @Value("${site.name:Schooner}") + private String siteName; + + @Value("${site.description:a schooner forum}") + private String siteDescription; + + public User getCurrentUser(Authentication auth) { + if (auth == null || !auth.isAuthenticated() || + "anonymousUser".equals(auth.getPrincipal())) { + return null; + } + return (User) auth.getPrincipal(); + } + + public void addCommonAttributes(Model model, Authentication auth) { + model.addAttribute("siteName", siteName); + model.addAttribute("siteDescription", siteDescription); + + User user = getCurrentUser(auth); + if (user != null) { + model.addAttribute("isLoggedIn", true); + model.addAttribute("currentUser", user.getUsername()); + model.addAttribute("unreadCount", + messageRepository.countByRecipientAndReadFalseAndDeletedByRecipientFalse(user)); + } else { + model.addAttribute("isLoggedIn", false); + model.addAttribute("unreadCount", 0L); + } + } +} From 418a6c49a079a9628dcfcd366d53ad3df2402b4c Mon Sep 17 00:00:00 2001 From: james Date: Wed, 8 Apr 2026 14:08:39 -0400 Subject: [PATCH 08/52] Html files for messages --- java/src/main/resources/static/style.css | 3 ++ .../resources/templates/messages/compose.html | 40 ++++++++++++++++++ .../resources/templates/messages/inbox.html | 41 ++++++++++++++++++ .../resources/templates/messages/outbox.html | 36 ++++++++++++++++ .../resources/templates/messages/view.html | 42 +++++++++++++++++++ 5 files changed, 162 insertions(+) create mode 100644 java/src/main/resources/templates/messages/compose.html create mode 100644 java/src/main/resources/templates/messages/inbox.html create mode 100644 java/src/main/resources/templates/messages/outbox.html create mode 100644 java/src/main/resources/templates/messages/view.html diff --git a/java/src/main/resources/static/style.css b/java/src/main/resources/static/style.css index d69c81d..e0b1847 100644 --- a/java/src/main/resources/static/style.css +++ b/java/src/main/resources/static/style.css @@ -19,3 +19,6 @@ body { padding: 10px; margin-bottom: 10px; } +.message-list .list-group-item { + border-left: 4px solid #6f42c1; +} diff --git a/java/src/main/resources/templates/messages/compose.html b/java/src/main/resources/templates/messages/compose.html new file mode 100644 index 0000000..66caaf2 --- /dev/null +++ b/java/src/main/resources/templates/messages/compose.html @@ -0,0 +1,40 @@ + + + + +
+
+ +

Compose Message

+ +
+
    +
  • Error
  • +
+
+ +
+
+ + +
+
+ + +
+
+ + +
+ + Cancel +
+ +
+ + diff --git a/java/src/main/resources/templates/messages/inbox.html b/java/src/main/resources/templates/messages/inbox.html new file mode 100644 index 0000000..41da984 --- /dev/null +++ b/java/src/main/resources/templates/messages/inbox.html @@ -0,0 +1,41 @@ + + + + +
+
+ +
+

Inbox

+ Compose +
+ + + +
+ No messages. +
+ + + +
+ + diff --git a/java/src/main/resources/templates/messages/outbox.html b/java/src/main/resources/templates/messages/outbox.html new file mode 100644 index 0000000..cc5ef9c --- /dev/null +++ b/java/src/main/resources/templates/messages/outbox.html @@ -0,0 +1,36 @@ + + + + +
+
+ +
+

Sent Messages

+ Compose +
+ +
+ Inbox +
+ +
+ No sent messages. +
+ + + +
+ + diff --git a/java/src/main/resources/templates/messages/view.html b/java/src/main/resources/templates/messages/view.html new file mode 100644 index 0000000..8e6a70b --- /dev/null +++ b/java/src/main/resources/templates/messages/view.html @@ -0,0 +1,42 @@ + + + + +
+
+ +
+ ← Back +
+ +
+
+
Subject
+ + From sender + to recipient + — time + +
+
+

Message content

+
+
+ +
+ Reply + +
+ + + +
+
+ +
+ + From d721271140ea19d7347cecfcdeea4394ef7225e0 Mon Sep 17 00:00:00 2001 From: james Date: Wed, 8 Apr 2026 14:12:30 -0400 Subject: [PATCH 09/52] Whitespace --- .../src/main/java/com/zipcode/stardust/model/Message.java | 8 -------- 1 file changed, 8 deletions(-) diff --git a/java/src/main/java/com/zipcode/stardust/model/Message.java b/java/src/main/java/com/zipcode/stardust/model/Message.java index f5f9d85..cc34087 100644 --- a/java/src/main/java/com/zipcode/stardust/model/Message.java +++ b/java/src/main/java/com/zipcode/stardust/model/Message.java @@ -72,28 +72,20 @@ public String getTimeString() { public Long getId() { return id; } public void setId(Long id) { this.id = id; } - public User getSender() { return sender; } public void setSender(User sender) { this.sender = sender; } - public User getRecipient() { return recipient; } public void setRecipient(User recipient) { this.recipient = recipient; } - public String getSubject() { return subject; } public void setSubject(String subject) { this.subject = subject; } - public String getContent() { return content; } public void setContent(String content) { this.content = content; } - public LocalDateTime getSentAt() { return sentAt; } public void setSentAt(LocalDateTime sentAt) { this.sentAt = sentAt; } - public boolean isRead() { return read; } public void setRead(boolean read) { this.read = read; } - public boolean isDeletedBySender() { return deletedBySender; } public void setDeletedBySender(boolean v) { this.deletedBySender = v; } - public boolean isDeletedByRecipient() { return deletedByRecipient; } public void setDeletedByRecipient(boolean v) { this.deletedByRecipient = v; } } From 552af70c99de91d449a6304b87c53f0bb66bc34e Mon Sep 17 00:00:00 2001 From: james Date: Wed, 8 Apr 2026 15:25:48 -0400 Subject: [PATCH 10/52] Simple security --- Stardust.db | Bin 32768 -> 36864 bytes .../stardust/config/SecurityConfig.java | 2 +- 2 files changed, 1 insertion(+), 1 deletion(-) diff --git a/Stardust.db b/Stardust.db index 48dfa9673dac572c860d82b1fcf6d830aca36752..fea4e21245563dd3642a07eae344e52408f7e417 100644 GIT binary patch delta 573 zcmZo@U}{*vG(lRBhk=2C6Nq7eb)t^3EDwWT-Y#Cg76uM(TL%6hz8>Cbe8D_Lyt%wC z+_oDV4{{kb8F8?SOG`7hSeGOw<)r4O78fU`!zfPYAXmo_SA`HqCm&Y@sNCeOe0udM zsX3`7sVVVEmGMQX$(aS2sd*&|N%{FXsfl?CdHE#@d8IiyItob2ic|AaQj4&tDN0RD z!6FP)UlN~KqEM0vw6`QNw*bl5V)N3ZtkmQZg|ft=fOX82DFg78IDr9}&jL!O$qoD$dENte~Y;0<=y`iCbe8D_Lyt%wC kn*{}aaBnW+nIycJML=LNi-E#s7J)zfj9i-;1^&Y*08&vD!vFvP diff --git a/java/src/main/java/com/zipcode/stardust/config/SecurityConfig.java b/java/src/main/java/com/zipcode/stardust/config/SecurityConfig.java index 68a847c..9e1259d 100644 --- a/java/src/main/java/com/zipcode/stardust/config/SecurityConfig.java +++ b/java/src/main/java/com/zipcode/stardust/config/SecurityConfig.java @@ -38,7 +38,7 @@ public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { "/action_createaccount", "/static/**", "/style.css").permitAll() .requestMatchers("/addpost", "/action_post", "/action_comment") .authenticated() - .anyRequest().permitAll() + .anyRequest().authenticated() ) .formLogin(form -> form .loginPage("/loginform") From cb7934a32c2ae177f00a9ad40a8a1ed348782de4 Mon Sep 17 00:00:00 2001 From: joseph Date: Wed, 8 Apr 2026 16:21:09 -0400 Subject: [PATCH 11/52] changes --- Stardust.db | Bin 0 -> 40960 bytes java/pom.xml | 4 +- .../java/com/zipcode/stardust/model/User.java | 2 +- .../zipcode/stardust/model/UserProfile.java | 72 ++++++++++++++++++ .../repository/UserProfileRepository.java | 12 +++ .../stardust/service/ForumService.java | 25 +++++- .../src/main/resources/application.properties | 3 +- .../StardustApplicationTests.java} | 4 +- 8 files changed, 115 insertions(+), 7 deletions(-) create mode 100644 Stardust.db create mode 100644 java/src/main/java/com/zipcode/stardust/model/UserProfile.java create mode 100644 java/src/main/java/com/zipcode/stardust/repository/UserProfileRepository.java rename java/src/test/java/com/zipcode/{circuscircus/CircusCircusApplicationTests.java => Stardust/StardustApplicationTests.java} (85%) diff --git a/Stardust.db b/Stardust.db new file mode 100644 index 0000000000000000000000000000000000000000..c1fc97802d44c072f271fe85f2a60ee6e734933a GIT binary patch literal 40960 zcmeI)&u-&H90%}moxj~pyc~)Tt4gCSQnuOvX;FoM!_w`R9$2)ZyKvzUP2$;Pz@AAx zV@i(+969p>yaFcv|D*g>GP7Sv@b;wGw``b|J>Li>KDAyzsyrt2+MB zuZHXE-sB)R(pk?A(6VH{E)}fixkasr>wYRmMkrZ`r%i_ESv7ayPyddT_owsl~+`Kbi6#n?0R}F99_9k!VQS?=k*w)QfD}`8Qng3%r zF4q6l+J=L_<=(JB00Izz00bZa0SG_<0uX=z1R!u-1u*`z{}fB*y_009U< z00Izz00d42-1xuT{G9|p2S>sE=HE7n1p*L&00bZa0SG_<0uX=z1R(HQ2)tFUko&ce zJQTX0X-$kD@Wig!{r->t|9i>EC>{b3fB*y_009U<00Izz00ba##R3@rU-1P%^$>sn z1Rwwb2tWV=5P$##AOL|&5Ww^Qmw=2aAOHafKmY;|fB*y_009U<00LJmfZzYG_yVAM Z2tWV=5P$##AOHafKmY;|fWRdP`~y758&?1T literal 0 HcmV?d00001 diff --git a/java/pom.xml b/java/pom.xml index d35d08a..9026094 100644 --- a/java/pom.xml +++ b/java/pom.xml @@ -45,12 +45,12 @@ org.xerial sqlite-jdbc - 3.45.3.0 + org.hibernate.orm hibernate-community-dialects - 6.4.4.Final + org.springframework.boot diff --git a/java/src/main/java/com/zipcode/stardust/model/User.java b/java/src/main/java/com/zipcode/stardust/model/User.java index 7b02b2d..920911a 100644 --- a/java/src/main/java/com/zipcode/stardust/model/User.java +++ b/java/src/main/java/com/zipcode/stardust/model/User.java @@ -50,7 +50,7 @@ public User(String email, String username, String rawPassword, PasswordEncoder e this.email = email; this.username = username; this.passwordHash = encoder.encode(rawPassword); - this.admin = "admin".equalsIgnoreCase(username); + this.admin = "admin".equalsIgnoreCase(username); ///meanifull } public boolean checkPassword(String rawPassword, PasswordEncoder encoder) { diff --git a/java/src/main/java/com/zipcode/stardust/model/UserProfile.java b/java/src/main/java/com/zipcode/stardust/model/UserProfile.java new file mode 100644 index 0000000..7f49316 --- /dev/null +++ b/java/src/main/java/com/zipcode/stardust/model/UserProfile.java @@ -0,0 +1,72 @@ +package com.zipcode.stardust.model; + +import jakarta.persistence.*; +import java.time.LocalDateTime; + +@Entity +@Table(name = "user_profiles") +public class UserProfile { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + private String bio; + + private String email; + + private LocalDateTime joinDate; + + @OneToOne + @JoinColumn(name = "user_id") + private User user; + + public UserProfile() {} + + public UserProfile(User user) { + this.user = user; + this.email = user.getEmail(); + this.joinDate = LocalDateTime.now(); + } + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public String getBio() { + return bio; + } + + public void setBio(String bio) { + this.bio = bio; + } + + public String getEmail() { + return email; + } + + public void setEmail(String email) { + this.email = email; + } + + public LocalDateTime getJoinDate() { + return joinDate; + } + + public void setJoinDate(LocalDateTime joinDate) { + this.joinDate = joinDate; + } + + public User getUser() { + return user; + } + + public void setUser(User user) { + this.user = user; + } + +} \ No newline at end of file diff --git a/java/src/main/java/com/zipcode/stardust/repository/UserProfileRepository.java b/java/src/main/java/com/zipcode/stardust/repository/UserProfileRepository.java new file mode 100644 index 0000000..59e14c4 --- /dev/null +++ b/java/src/main/java/com/zipcode/stardust/repository/UserProfileRepository.java @@ -0,0 +1,12 @@ +package com.zipcode.stardust.repository; +import com.zipcode.stardust.model.UserProfile; + +import com.zipcode.stardust.model.User; +import org.springframework.data.jpa.repository.JpaRepository; + +public interface UserProfileRepository extends JpaRepository { + + + + UserProfile findByUser(User user); +} \ No newline at end of file diff --git a/java/src/main/java/com/zipcode/stardust/service/ForumService.java b/java/src/main/java/com/zipcode/stardust/service/ForumService.java index 1ebbc72..46c9b9b 100644 --- a/java/src/main/java/com/zipcode/stardust/service/ForumService.java +++ b/java/src/main/java/com/zipcode/stardust/service/ForumService.java @@ -1,8 +1,11 @@ package com.zipcode.stardust.service; import com.zipcode.stardust.model.Subforum; +import com.zipcode.stardust.model.UserProfile; +import com.zipcode.stardust.model.User; import com.zipcode.stardust.repository.SubforumRepository; import com.zipcode.stardust.repository.UserRepository; +import com.zipcode.stardust.repository.UserProfileRepository; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import org.springframework.web.util.HtmlUtils; @@ -18,6 +21,9 @@ public class ForumService { @Autowired private UserRepository userRepository; + @Autowired + private UserProfileRepository userProfileRepository; // ADD 1 - new repo + public String generateLinkPath(Long subforumId) { StringBuilder sb = new StringBuilder(); sb.append(" / Forum Index"); @@ -63,4 +69,21 @@ public boolean usernameTaken(String username) { public boolean emailTaken(String email) { return userRepository.existsByEmail(email); } -} + + // ADD 2 - UserProfile methods + public UserProfile getUserProfile(User user) { + return userProfileRepository.findByUser(user); + } + + public UserProfile createUserProfile(User user) { + UserProfile profile = new UserProfile(user); + return userProfileRepository.save(profile); + } + + public UserProfile updateBio(User user, String bio) { + UserProfile profile = userProfileRepository.findByUser(user); + profile.setBio(bio); + return userProfileRepository.save(profile); + } + +} \ No newline at end of file diff --git a/java/src/main/resources/application.properties b/java/src/main/resources/application.properties index 3a5f22b..fa02e53 100644 --- a/java/src/main/resources/application.properties +++ b/java/src/main/resources/application.properties @@ -3,8 +3,9 @@ spring.datasource.driver-class-name=org.sqlite.JDBC spring.jpa.database-platform=org.hibernate.community.dialect.SQLiteDialect spring.jpa.hibernate.ddl-auto=update spring.jpa.show-sql=false -server.port=5000 +server.port=8080 spring.application.name=Schooner site.name=Schooner site.description=a schooner forum spring.thymeleaf.cache=false + diff --git a/java/src/test/java/com/zipcode/circuscircus/CircusCircusApplicationTests.java b/java/src/test/java/com/zipcode/Stardust/StardustApplicationTests.java similarity index 85% rename from java/src/test/java/com/zipcode/circuscircus/CircusCircusApplicationTests.java rename to java/src/test/java/com/zipcode/Stardust/StardustApplicationTests.java index 2ffc944..ed6bd9e 100644 --- a/java/src/test/java/com/zipcode/circuscircus/CircusCircusApplicationTests.java +++ b/java/src/test/java/com/zipcode/Stardust/StardustApplicationTests.java @@ -1,4 +1,4 @@ -package com.zipcode.stardust; +package com.zipcode.Stardust; import org.junit.jupiter.api.Test; import org.springframework.boot.test.context.SpringBootTest; @@ -7,4 +7,4 @@ class StardustApplicationTests { @Test void contextLoads() {} -} +} \ No newline at end of file From d762babd4dd33370b11de8ea102d19ad0a924860 Mon Sep 17 00:00:00 2001 From: james Date: Wed, 8 Apr 2026 16:46:28 -0400 Subject: [PATCH 12/52] userProfileRepository --- .../zipcode/stardust/service/ForumService.java | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/java/src/main/java/com/zipcode/stardust/service/ForumService.java b/java/src/main/java/com/zipcode/stardust/service/ForumService.java index d0e09e7..ecd817a 100644 --- a/java/src/main/java/com/zipcode/stardust/service/ForumService.java +++ b/java/src/main/java/com/zipcode/stardust/service/ForumService.java @@ -1,10 +1,7 @@ package com.zipcode.stardust.service; -import com.zipcode.stardust.model.Subforum; -import com.zipcode.stardust.model.UserProfile; -import com.zipcode.stardust.model.User; -import com.zipcode.stardust.repository.SubforumRepository; -import com.zipcode.stardust.repository.UserRepository; +import java.util.Optional; + import org.commonmark.node.Node; import org.commonmark.parser.Parser; import org.commonmark.renderer.html.HtmlRenderer; @@ -14,7 +11,12 @@ import org.springframework.stereotype.Service; import org.springframework.web.util.HtmlUtils; -import java.util.Optional; +import com.zipcode.stardust.model.Subforum; +import com.zipcode.stardust.model.User; +import com.zipcode.stardust.model.UserProfile; +import com.zipcode.stardust.repository.SubforumRepository; +import com.zipcode.stardust.repository.UserProfileRepository; +import com.zipcode.stardust.repository.UserRepository; @Service public class ForumService { @@ -25,6 +27,9 @@ public class ForumService { @Autowired private UserRepository userRepository; + @Autowired + private UserProfileRepository userProfileRepository; + // Markdown rendering pipeline — thread-safe singletons private final Parser mdParser = Parser.builder().build(); private final HtmlRenderer mdRenderer = HtmlRenderer.builder().build(); @@ -106,5 +111,4 @@ public UserProfile updateBio(User user, String bio) { profile.setBio(bio); return userProfileRepository.save(profile); } - } \ No newline at end of file From 0446d2799888b9d55bd61168845539748454225b Mon Sep 17 00:00:00 2001 From: james Date: Wed, 8 Apr 2026 21:32:05 -0400 Subject: [PATCH 13/52] Checkpoint --- Stardust.db | Bin 36864 -> 45056 bytes .../stardust/controller/ForumController.java | 24 +++--------------- java/src/main/resources/templates/header.html | 4 +++ 3 files changed, 8 insertions(+), 20 deletions(-) diff --git a/Stardust.db b/Stardust.db index fea4e21245563dd3642a07eae344e52408f7e417..b4c6fafce83189545d587daf39ef33444d7cef58 100644 GIT binary patch delta 268 zcmZozz|`=7X@ayMF9QPuHxR=B+e95>MP3HIyj{F}Eeu>-z6|_9d_BC=_=0(gcyoDO zxNW(7xtTULp5;txG~#3zS663jQZ7kM%1JFPPA!TrD9TUE%tUxUc$TFJ4&*m^qH%6Ssn(xyj{F}Eessowha71d_BC=_=0(gcyoDO RxNSEJ3LNCxY|DMP1puyT4k`cu diff --git a/java/src/main/java/com/zipcode/stardust/controller/ForumController.java b/java/src/main/java/com/zipcode/stardust/controller/ForumController.java index 61a0d85..72f27b3 100644 --- a/java/src/main/java/com/zipcode/stardust/controller/ForumController.java +++ b/java/src/main/java/com/zipcode/stardust/controller/ForumController.java @@ -2,9 +2,9 @@ import com.zipcode.stardust.model.*; import com.zipcode.stardust.repository.*; +import com.zipcode.stardust.service.CommonAttributesHelper; import com.zipcode.stardust.service.ForumService; import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.beans.factory.annotation.Value; import org.springframework.security.core.Authentication; import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.stereotype.Controller; @@ -26,30 +26,14 @@ public class ForumController { @Autowired private UserRepository userRepository; @Autowired private PasswordEncoder passwordEncoder; @Autowired private ForumService forumService; - - @Value("${site.name:Schooner}") - private String siteName; - - @Value("${site.description:a schooner forum}") - private String siteDescription; + @Autowired private CommonAttributesHelper helper; private User getCurrentUser(Authentication auth) { - if (auth == null || !auth.isAuthenticated() || - "anonymousUser".equals(auth.getPrincipal())) { - return null; - } - return (User) auth.getPrincipal(); + return helper.getCurrentUser(auth); } private void addCommonAttributes(Model model, Authentication auth) { - model.addAttribute("siteName", siteName); - model.addAttribute("siteDescription", siteDescription); - if (auth != null && auth.isAuthenticated() && !"anonymousUser".equals(auth.getPrincipal())) { - model.addAttribute("currentUser", auth.getName()); - model.addAttribute("isLoggedIn", true); - } else { - model.addAttribute("isLoggedIn", false); - } + helper.addCommonAttributes(model, auth); } @GetMapping("/") diff --git a/java/src/main/resources/templates/header.html b/java/src/main/resources/templates/header.html index 8abe4a2..3299cf5 100644 --- a/java/src/main/resources/templates/header.html +++ b/java/src/main/resources/templates/header.html @@ -8,6 +8,10 @@ - - - diff --git a/java/src/main/resources/templates/editpost.html b/java/src/main/resources/templates/editpost.html index f0855b1..450091f 100644 --- a/java/src/main/resources/templates/editpost.html +++ b/java/src/main/resources/templates/editpost.html @@ -14,6 +14,8 @@

Edit Post

+
@@ -298,6 +302,54 @@ ta.focus(); } + const csrf = document.querySelector('meta[name="_csrf"]')?.content; + const csrfHdr = document.querySelector('meta[name="_csrf_header"]')?.content; + + function insertAtCursor(taId, text) { + const ta = document.getElementById(taId); + const pos = ta.selectionStart; + ta.value = ta.value.substring(0, pos) + text + ta.value.substring(pos); + ta.focus(); + } + function sendUpload(taId, file) { + const fd = new FormData(); + fd.append('file', file); + const headers = {}; + if (csrf && csrfHdr) headers[csrfHdr] = csrf; + fetch('/upload', { method: 'POST', body: fd, headers }) + .then(r => r.json()) + .then(data => { + if (data.error) { alert(data.error); return; } + const markup = data.type === 'video' + ? '\n' + : '![](' + data.url + ')\n'; + insertAtCursor(taId, markup); + }) + .catch(() => alert('Upload failed.')); + } + function triggerUpload(taId) { + const input = document.getElementById('upload-input'); + input.onchange = () => { if (input.files[0]) sendUpload(taId, input.files[0]); input.value = ''; }; + input.click(); + } + document.addEventListener('DOMContentLoaded', () => { + ['new-comment'].forEach(id => { + const ta = document.getElementById(id); + if (!ta) return; + ta.addEventListener('paste', e => { + const items = e.clipboardData?.items; + if (!items) return; + for (const item of items) { + if (item.kind === 'file' && item.type.startsWith('image/')) { + e.preventDefault(); + sendUpload(id, item.getAsFile()); + break; + } + } + }); + }); + }); + let pendingDeleteFormId = null; function confirmDelete(formId) { From e890948e921ac98e262cee283100540579a59106 Mon Sep 17 00:00:00 2001 From: mahala Date: Mon, 13 Apr 2026 00:24:11 -0400 Subject: [PATCH 50/52] Debug Controllers 1 --- .../zipcode/stardust/controller/ForumController.java | 9 ++++++--- .../stardust/controller/MediaEmbedController.java | 4 ++-- .../zipcode/stardust/controller/UploadController.java | 11 ++++++----- 3 files changed, 14 insertions(+), 10 deletions(-) diff --git a/java/src/main/java/com/zipcode/stardust/controller/ForumController.java b/java/src/main/java/com/zipcode/stardust/controller/ForumController.java index ec4186c..617ce67 100644 --- a/java/src/main/java/com/zipcode/stardust/controller/ForumController.java +++ b/java/src/main/java/com/zipcode/stardust/controller/ForumController.java @@ -313,9 +313,10 @@ public String deleteComment(@RequestParam Long commentId, Authentication auth) { User user = getCurrentUser(auth); if (user == null) return "redirect:/loginform"; - Optional opt = commentRepository.findById(commentId); + if (commentId == null) return "redirect:/"; + Optional opt = commentRepository.findById(commentId); if (opt.isEmpty()) return "redirect:/viewpost?post=" + postId; - com.zipcode.stardust.model.Comment comment = opt.get(); + Comment comment = opt.get(); boolean isOwner = comment.getUser().getUsername().equals(user.getUsername()); if (!isOwner && !user.isAdmin()) return "redirect:/viewpost?post=" + postId; forumService.moderateComment(commentId); @@ -356,7 +357,9 @@ public String saveEditComment(@RequestParam Long commentId, Authentication auth) { User user = getCurrentUser(auth); if (user == null) return "redirect:/loginform"; - forumService.editComment(commentId, content, user); + if (commentId == null) return "redirect:/"; + boolean saved = forumService.editComment(commentId, content, user); + if (!saved) return "redirect:/viewpost?post=" + postId + "&editError=1"; return "redirect:/viewpost?post=" + postId; } } \ No newline at end of file diff --git a/java/src/main/java/com/zipcode/stardust/controller/MediaEmbedController.java b/java/src/main/java/com/zipcode/stardust/controller/MediaEmbedController.java index b39727c..4d22059 100644 --- a/java/src/main/java/com/zipcode/stardust/controller/MediaEmbedController.java +++ b/java/src/main/java/com/zipcode/stardust/controller/MediaEmbedController.java @@ -1,5 +1,5 @@ package com.zipcode.stardust.controller; - + import java.util.Optional; import org.springframework.beans.factory.annotation.Autowired; @@ -15,7 +15,7 @@ import com.zipcode.stardust.model.User; import com.zipcode.stardust.repository.PostRepository; import com.zipcode.stardust.service.MediaEmbedService; - + /** * ============================================================= * MediaEmbedController — handles HTTP requests related to diff --git a/java/src/main/java/com/zipcode/stardust/controller/UploadController.java b/java/src/main/java/com/zipcode/stardust/controller/UploadController.java index b3e1d26..b91b5a0 100644 --- a/java/src/main/java/com/zipcode/stardust/controller/UploadController.java +++ b/java/src/main/java/com/zipcode/stardust/controller/UploadController.java @@ -34,20 +34,21 @@ public class UploadController { @PostMapping("/upload") public ResponseEntity upload(@RequestParam MultipartFile file, Authentication auth) { - if (auth == null || !auth.isAuthenticated()) { + if (auth == null || !auth.isAuthenticated() + || "anonymousUser".equals(auth.getPrincipal())) { return ResponseEntity.status(401).body(Map.of("error", "Login required.")); } + if (file.isEmpty()) { + return ResponseEntity.badRequest().body(Map.of("error", "File is empty.")); + } + String contentType = file.getContentType(); if (contentType == null || !ALLOWED_TYPES.contains(contentType)) { return ResponseEntity.badRequest() .body(Map.of("error", "Unsupported file type: " + contentType)); } - if (file.isEmpty()) { - return ResponseEntity.badRequest().body(Map.of("error", "File is empty.")); - } - String ext = getExtension(file.getOriginalFilename(), contentType); String filename = UUID.randomUUID() + ext; From 1013e3c974f58e69127ae3329b7ff1c4566afef0 Mon Sep 17 00:00:00 2001 From: mahala Date: Mon, 13 Apr 2026 00:32:20 -0400 Subject: [PATCH 51/52] Message Markdown --- .../com/zipcode/stardust/controller/MessageController.java | 3 +++ java/src/main/resources/templates/messages/view.html | 4 +--- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/java/src/main/java/com/zipcode/stardust/controller/MessageController.java b/java/src/main/java/com/zipcode/stardust/controller/MessageController.java index f987f41..f12a282 100644 --- a/java/src/main/java/com/zipcode/stardust/controller/MessageController.java +++ b/java/src/main/java/com/zipcode/stardust/controller/MessageController.java @@ -5,6 +5,7 @@ import com.zipcode.stardust.repository.MessageRepository; import com.zipcode.stardust.repository.UserRepository; import com.zipcode.stardust.service.CommonAttributesHelper; +import com.zipcode.stardust.service.ForumService; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.security.core.Authentication; import org.springframework.stereotype.Controller; @@ -23,6 +24,7 @@ public class MessageController { @Autowired private MessageRepository messageRepository; @Autowired private UserRepository userRepository; @Autowired private CommonAttributesHelper helper; + @Autowired private ForumService forumService; // ── Inbox ──────────────────────────────────────────────────────────────── @@ -143,6 +145,7 @@ public String viewMessage( + "&subject=" + encodedSubject; model.addAttribute("message", message); + model.addAttribute("renderedBody", forumService.renderMarkdown(message.getContent())); model.addAttribute("replyUrl", replyUrl); model.addAttribute("isSender", isSender); return "messages/view"; diff --git a/java/src/main/resources/templates/messages/view.html b/java/src/main/resources/templates/messages/view.html index 8e6a70b..0e6288a 100644 --- a/java/src/main/resources/templates/messages/view.html +++ b/java/src/main/resources/templates/messages/view.html @@ -20,9 +20,7 @@
Subject
time
-
-

Message content

-
+
Message content
From 1d9edf49dd18716446f6227017c6a4f9f7b62305 Mon Sep 17 00:00:00 2001 From: mahala Date: Mon, 13 Apr 2026 01:07:40 -0400 Subject: [PATCH 52/52] Error reduction --- .../stardust/controller/ForumController.java | 32 +++++++++--------- .../controller/MediaEmbedController.java | 6 ++-- .../controller/MessageController.java | 4 +-- .../stardust/controller/UploadController.java | 21 ++++++------ .../stardust/service/ForumService.java | 10 +++--- .../stardust/service/MediaEmbedService.java | 6 ++-- .../main/resources/templates/createpost.html | 23 ++++++++----- .../main/resources/templates/editpost.html | 23 ++++++++----- .../resources/templates/messages/compose.html | 23 ++++++++----- .../main/resources/templates/viewpost.html | 33 ++++++++++++------- 10 files changed, 106 insertions(+), 75 deletions(-) diff --git a/java/src/main/java/com/zipcode/stardust/controller/ForumController.java b/java/src/main/java/com/zipcode/stardust/controller/ForumController.java index 617ce67..1f96fa1 100644 --- a/java/src/main/java/com/zipcode/stardust/controller/ForumController.java +++ b/java/src/main/java/com/zipcode/stardust/controller/ForumController.java @@ -59,7 +59,7 @@ public String index(Model model, Authentication auth) { } @GetMapping("/subforum") - public String subforum(@RequestParam Long sub, Model model, Authentication auth) { + public String subforum(@RequestParam long sub, Model model, Authentication auth) { addCommonAttributes(model, auth); Optional opt = subforumRepository.findById(sub); if (opt.isEmpty()) return "redirect:/"; @@ -119,7 +119,7 @@ public String createAccount(@RequestParam String username, } @GetMapping("/addpost") - public String addPostForm(@RequestParam Long sub, Model model, Authentication auth) { + public String addPostForm(@RequestParam long sub, Model model, Authentication auth) { addCommonAttributes(model, auth); Optional opt = subforumRepository.findById(sub); if (opt.isEmpty()) return "redirect:/"; @@ -129,7 +129,7 @@ public String addPostForm(@RequestParam Long sub, Model model, Authentication au } @PostMapping("/action_post") - public String createPost(@RequestParam Long sub, + public String createPost(@RequestParam long sub, @RequestParam String title, @RequestParam String content, Model model, Authentication auth) { @@ -163,7 +163,7 @@ public String createPost(@RequestParam Long sub, } @GetMapping("/viewpost") - public String viewPost(@RequestParam Long post, Model model, Authentication auth) { + public String viewPost(@RequestParam long post, Model model, Authentication auth) { addCommonAttributes(model, auth); Optional opt = postRepository.findById(post); if (opt.isEmpty()) return "redirect:/"; @@ -203,7 +203,7 @@ public String viewPost(@RequestParam Long post, Model model, Authentication auth } @PostMapping("/action_comment") - public String addComment(@RequestParam Long post, + public String addComment(@RequestParam long post, @RequestParam String content, Authentication auth) { if (auth == null || !auth.isAuthenticated()) { @@ -218,7 +218,7 @@ public String addComment(@RequestParam Long post, } @GetMapping("/action_comment") - public String addCommentGet(@RequestParam Long post) { + public String addCommentGet(@RequestParam long post) { return "redirect:/viewpost?post=" + post; } @@ -276,7 +276,7 @@ public String updatePassword(@RequestParam String currentPassword, } @PostMapping("/action_react") - public String reactToPost(@RequestParam Long postId, + public String reactToPost(@RequestParam long postId, @RequestParam String type, Authentication auth) { if (auth == null || !auth.isAuthenticated()) { @@ -293,7 +293,7 @@ public String reactToPost(@RequestParam Long postId, // ── Delete post ─────────────────────────────────────────────── @PostMapping("/action_delete_post") - public String deletePost(@RequestParam Long postId, Authentication auth) { + public String deletePost(@RequestParam long postId, Authentication auth) { User user = getCurrentUser(auth); if (user == null) return "redirect:/loginform"; Optional opt = postRepository.findById(postId); @@ -301,19 +301,18 @@ public String deletePost(@RequestParam Long postId, Authentication auth) { Post post = opt.get(); boolean isOwner = post.getUser().getUsername().equals(user.getUsername()); if (!isOwner && !user.isAdmin()) return "redirect:/viewpost?post=" + postId; - Long subId = post.getSubforum().getId(); + long subId = post.getSubforum().getId(); forumService.moderatePost(postId); return "redirect:/subforum?sub=" + subId; } // ── Delete comment ──────────────────────────────────────────── @PostMapping("/action_delete_comment") - public String deleteComment(@RequestParam Long commentId, - @RequestParam Long postId, + public String deleteComment(@RequestParam long commentId, + @RequestParam long postId, Authentication auth) { User user = getCurrentUser(auth); if (user == null) return "redirect:/loginform"; - if (commentId == null) return "redirect:/"; Optional opt = commentRepository.findById(commentId); if (opt.isEmpty()) return "redirect:/viewpost?post=" + postId; Comment comment = opt.get(); @@ -325,7 +324,7 @@ public String deleteComment(@RequestParam Long commentId, // ── Edit post form ──────────────────────────────────────────── @GetMapping("/editpost") - public String editPostForm(@RequestParam Long postId, Model model, Authentication auth) { + public String editPostForm(@RequestParam long postId, Model model, Authentication auth) { User user = getCurrentUser(auth); if (user == null) return "redirect:/loginform"; Optional opt = postRepository.findById(postId); @@ -339,7 +338,7 @@ public String editPostForm(@RequestParam Long postId, Model model, Authenticatio // ── Save edited post ────────────────────────────────────────── @PostMapping("/action_edit_post") - public String saveEditPost(@RequestParam Long postId, + public String saveEditPost(@RequestParam long postId, @RequestParam String title, @RequestParam String content, Authentication auth) { @@ -351,13 +350,12 @@ public String saveEditPost(@RequestParam Long postId, // ── Save edited comment ─────────────────────────────────────── @PostMapping("/action_edit_comment") - public String saveEditComment(@RequestParam Long commentId, - @RequestParam Long postId, + public String saveEditComment(@RequestParam long commentId, + @RequestParam long postId, @RequestParam String content, Authentication auth) { User user = getCurrentUser(auth); if (user == null) return "redirect:/loginform"; - if (commentId == null) return "redirect:/"; boolean saved = forumService.editComment(commentId, content, user); if (!saved) return "redirect:/viewpost?post=" + postId + "&editError=1"; return "redirect:/viewpost?post=" + postId; diff --git a/java/src/main/java/com/zipcode/stardust/controller/MediaEmbedController.java b/java/src/main/java/com/zipcode/stardust/controller/MediaEmbedController.java index 4d22059..c9e5ce5 100644 --- a/java/src/main/java/com/zipcode/stardust/controller/MediaEmbedController.java +++ b/java/src/main/java/com/zipcode/stardust/controller/MediaEmbedController.java @@ -85,7 +85,7 @@ private User getCurrentUser(Authentication auth) { */ @PostMapping("/action_add_embed") public String addEmbed( - @RequestParam("post") Long postId, + @RequestParam("post") long postId, @RequestParam("url") String url, @RequestParam(value = "mediaType", required = false) String mediaTypeOverride, @@ -160,8 +160,8 @@ public String addEmbed( */ @PostMapping("/action_delete_embed") public String deleteEmbed( - @RequestParam("embedId") Long embedId, - @RequestParam("post") Long postId, + @RequestParam("embedId") long embedId, + @RequestParam("post") long postId, Authentication auth, RedirectAttributes redirectAttrs) { diff --git a/java/src/main/java/com/zipcode/stardust/controller/MessageController.java b/java/src/main/java/com/zipcode/stardust/controller/MessageController.java index f12a282..b51b311 100644 --- a/java/src/main/java/com/zipcode/stardust/controller/MessageController.java +++ b/java/src/main/java/com/zipcode/stardust/controller/MessageController.java @@ -117,7 +117,7 @@ public String sendMessage( @GetMapping("/messages/view") public String viewMessage( - @RequestParam Long id, + @RequestParam long id, Model model, Authentication auth) { helper.addCommonAttributes(model, auth); @@ -155,7 +155,7 @@ public String viewMessage( @PostMapping("/messages/delete") public String deleteMessage( - @RequestParam Long id, + @RequestParam long id, @RequestParam String box, Authentication auth) { diff --git a/java/src/main/java/com/zipcode/stardust/controller/UploadController.java b/java/src/main/java/com/zipcode/stardust/controller/UploadController.java index b91b5a0..f3c4333 100644 --- a/java/src/main/java/com/zipcode/stardust/controller/UploadController.java +++ b/java/src/main/java/com/zipcode/stardust/controller/UploadController.java @@ -7,7 +7,6 @@ import java.nio.file.Paths; import java.nio.file.StandardCopyOption; import java.util.Map; -import java.util.Set; import java.util.UUID; import org.springframework.beans.factory.annotation.Value; @@ -24,12 +23,12 @@ public class UploadController { @Value("${upload.dir:uploads}") private String uploadDir; - private static final Set ALLOWED_TYPES = Set.of( - "image/jpeg", "image/png", "image/gif", "image/webp", - "image/bmp", "image/svg+xml", "image/avif", - "video/mp4", "video/webm", "video/ogg", - "video/quicktime", "video/x-msvideo", "video/x-matroska" - ); + /** Accept any image/* or video/* type except SVG (can carry embedded scripts). */ + private boolean isAllowedType(String contentType) { + if (contentType == null) return false; + if ("image/svg+xml".equals(contentType)) return false; + return contentType.startsWith("image/") || contentType.startsWith("video/"); + } @PostMapping("/upload") public ResponseEntity upload(@RequestParam MultipartFile file, @@ -43,11 +42,13 @@ public ResponseEntity upload(@RequestParam MultipartFile file, return ResponseEntity.badRequest().body(Map.of("error", "File is empty.")); } - String contentType = file.getContentType(); - if (contentType == null || !ALLOWED_TYPES.contains(contentType)) { + String rawType = file.getContentType(); + if (!isAllowedType(rawType)) { return ResponseEntity.badRequest() - .body(Map.of("error", "Unsupported file type: " + contentType)); + .body(Map.of("error", "Unsupported file type: " + rawType)); } + // isAllowedType returned true, so rawType is non-null here + String contentType = rawType; String ext = getExtension(file.getOriginalFilename(), contentType); String filename = UUID.randomUUID() + ext; diff --git a/java/src/main/java/com/zipcode/stardust/service/ForumService.java b/java/src/main/java/com/zipcode/stardust/service/ForumService.java index 0c62dd6..cbbfe29 100644 --- a/java/src/main/java/com/zipcode/stardust/service/ForumService.java +++ b/java/src/main/java/com/zipcode/stardust/service/ForumService.java @@ -89,7 +89,7 @@ public String renderMarkdown(String raw) { return sanitizer.sanitize(html); } - public String generateLinkPath(Long subforumId) { + public String generateLinkPath(long subforumId) { StringBuilder sb = new StringBuilder(); sb.append(" / Forum Index"); Optional opt = subforumRepository.findById(subforumId); @@ -221,15 +221,15 @@ public Reaction getUserReaction(User user, Post post) { return reactionRepository.findByUserAndPost(user, post); } - public void moderatePost(Long postId) { + public void moderatePost(long postId) { postRepository.deleteById(postId); } - public void moderateComment(Long commentId) { + public void moderateComment(long commentId) { commentRepository.deleteById(commentId); } - public boolean editPost(Long postId, String title, String content, User requestingUser) { + public boolean editPost(long postId, String title, String content, User requestingUser) { Optional opt = postRepository.findById(postId); if (opt.isEmpty()) return false; Post post = opt.get(); @@ -240,7 +240,7 @@ public boolean editPost(Long postId, String title, String content, User requesti return true; } - public boolean editComment(Long commentId, String content, User requestingUser) { + public boolean editComment(long commentId, String content, User requestingUser) { Optional opt = commentRepository.findById(commentId); if (opt.isEmpty()) return false; Comment comment = opt.get(); diff --git a/java/src/main/java/com/zipcode/stardust/service/MediaEmbedService.java b/java/src/main/java/com/zipcode/stardust/service/MediaEmbedService.java index fa8bf8e..e4e11a0 100644 --- a/java/src/main/java/com/zipcode/stardust/service/MediaEmbedService.java +++ b/java/src/main/java/com/zipcode/stardust/service/MediaEmbedService.java @@ -8,6 +8,7 @@ import org.springframework.stereotype.Service; import java.util.List; +import org.springframework.lang.NonNull; /** * ============================================================= @@ -127,7 +128,8 @@ public boolean isValidCaption(String caption) { * @param embed the embed to persist * @return the saved embed (id is now populated) */ - public MediaEmbed save(MediaEmbed embed) { + @NonNull + public MediaEmbed save(@NonNull MediaEmbed embed) { return mediaEmbedRepository.save(embed); } @@ -148,7 +150,7 @@ public List getEmbedsForPost(Post post) { * * @param embedId the id of the embed to remove */ - public void deleteEmbed(Long embedId) { + public void deleteEmbed(long embedId) { mediaEmbedRepository.deleteById(embedId); } } diff --git a/java/src/main/resources/templates/createpost.html b/java/src/main/resources/templates/createpost.html index 8554fb6..7742468 100644 --- a/java/src/main/resources/templates/createpost.html +++ b/java/src/main/resources/templates/createpost.html @@ -21,8 +21,9 @@

Create Post in Forum

- + +
+ +
@@ -213,8 +217,9 @@
Comments (0)
No comments yet.
Add a Comment
- + +
@@ -242,8 +247,10 @@
Add a Comment
- + +
@@ -327,22 +334,24 @@ }) .catch(() => alert('Upload failed.')); } - function triggerUpload(taId) { - const input = document.getElementById('upload-input'); - input.onchange = () => { if (input.files[0]) sendUpload(taId, input.files[0]); input.value = ''; }; + function triggerUpload(taId, mode) { + const input = document.getElementById( + mode === 'folder' ? 'upload-folder-input' : 'upload-input'); + input.onchange = () => { + Array.from(input.files).forEach(f => sendUpload(taId, f)); + input.value = ''; + }; input.click(); } document.addEventListener('DOMContentLoaded', () => { - ['new-comment'].forEach(id => { - const ta = document.getElementById(id); - if (!ta) return; + document.querySelectorAll('textarea').forEach(ta => { ta.addEventListener('paste', e => { const items = e.clipboardData?.items; if (!items) return; for (const item of items) { - if (item.kind === 'file' && item.type.startsWith('image/')) { + if (item.kind === 'file' && (item.type.startsWith('image/') || item.type.startsWith('video/'))) { e.preventDefault(); - sendUpload(id, item.getAsFile()); + sendUpload(ta.id, item.getAsFile()); break; } }