diff --git a/CHANGELOG.md b/CHANGELOG.md index 0f47744..6c836ca 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,9 @@ Versionierung [semantisch](https://semver.org/lang/de/). ## [Unreleased] – 2026-07-14 · SysnaptiK Neural Rebrand ### Marke & Plattform +- **NeuralNet-MVP:** Der frühere Soziales-Platzhalter ist jetzt ein Opt-in-Hub für + Kontaktanfragen, Annahme/Ablehnung/Rückzug und private 1:1-Nachrichten. Ohne + bestätigte Verbindung lässt der Server keine Nachrichten zu. - **NeuralNet geplant:** Der kommende soziale Bereich heißt NeuralNet und priorisiert private Lernverbindungen: Kontaktanfragen müssen erst ausdrücklich angenommen werden, bevor private Nachrichten möglich sind. diff --git a/mindfluxfuture.md b/mindfluxfuture.md index e26efb1..9e09474 100644 --- a/mindfluxfuture.md +++ b/mindfluxfuture.md @@ -486,6 +486,10 @@ Der Verdacht „Profilbutton fehlt / Einstellungen nicht im Profil / irgendwas i ## Teil 3: NeuralNet – private Lernverbindungen & Nachrichten ⏳ in Umsetzung +> ✅ **MVP-Stand 2026-07-14:** Opt-in, Kontaktanfragen (annehmen, ablehnen, +> zurückziehen) und private Nachrichten nach bestätigter Verbindung sind umgesetzt. +> Blockieren, Melden/Moderation, Duelle und Lernlobby bleiben die nächsten Ausbauschritte. + > Ersetzt den `/soziales`-Platzhalter („Straßen? Wo wir hingehen, brauchen wir keine Straßen!"). Ziel: User können sich befreunden, intern miteinander schreiben, gemeinsam lernen und Achievements vergleichen – **vollständig selbst gehostet, ohne einen einzigen externen Dienst**, und DSGVO-konform von Grund auf. ### 3.1 Funktionsumfang diff --git a/src/main/java/com/example/mindflux/config/SecurityConfig.java b/src/main/java/com/example/mindflux/config/SecurityConfig.java index 2984140..abffaf3 100644 --- a/src/main/java/com/example/mindflux/config/SecurityConfig.java +++ b/src/main/java/com/example/mindflux/config/SecurityConfig.java @@ -42,7 +42,7 @@ public class SecurityConfig { .csrf(csrf -> csrf // Spiele-APIs werden per fetch (application/json) ohne Token aufgerufen -> ausnehmen. // Reine JSON-Endpunkte sind nicht über klassische HTML-Formulare CSRF-ausnutzbar. - .ignoringRequestMatchers("/api/games/**", "/api/techtacular/**", "/api/mindmatch/**", "/api/syntaxsaga/**", "/api/devlab/**", "/api/codeescape/**", "/api/bughunter/**", "/api/serviceclash/**", "/api/networker/**") + .ignoringRequestMatchers("/api/games/**", "/api/techtacular/**", "/api/mindmatch/**", "/api/syntaxsaga/**", "/api/devlab/**", "/api/codeescape/**", "/api/bughunter/**", "/api/serviceclash/**", "/api/networker/**", "/api/neuralnet/**") ); return http.build(); diff --git a/src/main/java/com/example/mindflux/controller/NeuralNetApiController.java b/src/main/java/com/example/mindflux/controller/NeuralNetApiController.java new file mode 100644 index 0000000..1182636 --- /dev/null +++ b/src/main/java/com/example/mindflux/controller/NeuralNetApiController.java @@ -0,0 +1,20 @@ +package com.example.mindflux.controller; +import com.example.mindflux.service.NeuralNetService; +import org.springframework.web.bind.annotation.*; +import java.security.Principal; +import java.util.Map; + +@RestController +@RequestMapping("/api/neuralnet") +public class NeuralNetApiController { + private final NeuralNetService service; + public NeuralNetApiController(NeuralNetService service) { this.service=service; } + @GetMapping("/overview") public NeuralNetService.Overview overview(Principal p) { return service.overview(p.getName()); } + @PostMapping("/enable") public void enable(Principal p) { service.enable(p.getName()); } + @PostMapping("/requests") public void request(Principal p,@RequestBody Map body) { service.request(p.getName(),body.get("username")); } + @PostMapping("/requests/{username}/accept") public void accept(Principal p,@PathVariable String username) { service.accept(p.getName(),username); } + @PostMapping("/requests/{username}/decline") public void decline(Principal p,@PathVariable String username) { service.decline(p.getName(),username); } + @DeleteMapping("/requests/{username}") public void cancel(Principal p,@PathVariable String username) { service.cancel(p.getName(),username); } + @GetMapping("/messages/{username}") public Object messages(Principal p,@PathVariable String username) { return service.conversation(p.getName(),username); } + @PostMapping("/messages") public Object send(Principal p,@RequestBody Map body) { return service.send(p.getName(),body.get("recipient"),body.get("body")); } +} diff --git a/src/main/java/com/example/mindflux/model/NeuralNetConnection.java b/src/main/java/com/example/mindflux/model/NeuralNetConnection.java new file mode 100644 index 0000000..7ae215e --- /dev/null +++ b/src/main/java/com/example/mindflux/model/NeuralNetConnection.java @@ -0,0 +1,21 @@ +package com.example.mindflux.model; + +import jakarta.persistence.*; +import java.time.LocalDateTime; + +@Entity +@Table(name = "neuralnet_connections") +public class NeuralNetConnection { + @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; + private String requester; + private String addressee; + private String status; + @Column(name = "created_at") private LocalDateTime createdAt; + @Column(name = "responded_at") private LocalDateTime respondedAt; + public NeuralNetConnection() {} + public NeuralNetConnection(String requester, String addressee) { this.requester=requester; this.addressee=addressee; this.status="PENDING"; this.createdAt=LocalDateTime.now(); } + public Long getId() { return id; } public String getRequester() { return requester; } public String getAddressee() { return addressee; } + public String getStatus() { return status; } public void setStatus(String status) { this.status=status; } + public LocalDateTime getCreatedAt() { return createdAt; } public LocalDateTime getRespondedAt() { return respondedAt; } + public void setRespondedAt(LocalDateTime respondedAt) { this.respondedAt=respondedAt; } +} diff --git a/src/main/java/com/example/mindflux/model/NeuralNetMessage.java b/src/main/java/com/example/mindflux/model/NeuralNetMessage.java new file mode 100644 index 0000000..0999d08 --- /dev/null +++ b/src/main/java/com/example/mindflux/model/NeuralNetMessage.java @@ -0,0 +1,21 @@ +package com.example.mindflux.model; + +import jakarta.persistence.*; +import java.time.LocalDateTime; + +@Entity +@Table(name = "neuralnet_messages") +public class NeuralNetMessage { + @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; + private String sender; + private String recipient; + private String body; + @Column(name = "sent_at") private LocalDateTime sentAt; + @Column(name = "read_at") private LocalDateTime readAt; + private boolean reported; + public NeuralNetMessage() {} + public NeuralNetMessage(String sender, String recipient, String body) { this.sender=sender; this.recipient=recipient; this.body=body; this.sentAt=LocalDateTime.now(); } + public Long getId() { return id; } public String getSender() { return sender; } public String getRecipient() { return recipient; } + public String getBody() { return body; } public LocalDateTime getSentAt() { return sentAt; } public LocalDateTime getReadAt() { return readAt; } + public void setReadAt(LocalDateTime readAt) { this.readAt=readAt; } public boolean isReported() { return reported; } public void setReported(boolean reported) { this.reported=reported; } +} diff --git a/src/main/java/com/example/mindflux/model/NeuralNetPrivacy.java b/src/main/java/com/example/mindflux/model/NeuralNetPrivacy.java new file mode 100644 index 0000000..2b204a5 --- /dev/null +++ b/src/main/java/com/example/mindflux/model/NeuralNetPrivacy.java @@ -0,0 +1,17 @@ +package com.example.mindflux.model; + +import jakarta.persistence.Entity; +import jakarta.persistence.Id; +import jakarta.persistence.Table; + +@Entity +@Table(name = "neuralnet_privacy") +public class NeuralNetPrivacy { + @Id private String username; + private boolean enabled; + public NeuralNetPrivacy() {} + public NeuralNetPrivacy(String username, boolean enabled) { this.username = username; this.enabled = enabled; } + public String getUsername() { return username; } + public boolean isEnabled() { return enabled; } + public void setEnabled(boolean enabled) { this.enabled = enabled; } +} diff --git a/src/main/java/com/example/mindflux/repository/NeuralNetConnectionRepository.java b/src/main/java/com/example/mindflux/repository/NeuralNetConnectionRepository.java new file mode 100644 index 0000000..75a0a40 --- /dev/null +++ b/src/main/java/com/example/mindflux/repository/NeuralNetConnectionRepository.java @@ -0,0 +1,14 @@ +package com.example.mindflux.repository; +import com.example.mindflux.model.NeuralNetConnection; +import org.springframework.data.jpa.repository.*; +import org.springframework.data.repository.query.Param; +import java.util.*; +public interface NeuralNetConnectionRepository extends JpaRepository { + Optional findByRequesterAndAddressee(String requester, String addressee); + List findByAddresseeAndStatusOrderByCreatedAtAsc(String addressee, String status); + List findByRequesterAndStatusOrderByCreatedAtAsc(String requester, String status); + @Query("select c from NeuralNetConnection c where c.status='ACCEPTED' and ((c.requester=:username) or (c.addressee=:username)) order by c.respondedAt desc") + List acceptedFor(@Param("username") String username); + @Query("select c from NeuralNetConnection c where c.status='ACCEPTED' and ((c.requester=:a and c.addressee=:b) or (c.requester=:b and c.addressee=:a))") + Optional acceptedBetween(@Param("a") String a, @Param("b") String b); +} diff --git a/src/main/java/com/example/mindflux/repository/NeuralNetMessageRepository.java b/src/main/java/com/example/mindflux/repository/NeuralNetMessageRepository.java new file mode 100644 index 0000000..f56ae64 --- /dev/null +++ b/src/main/java/com/example/mindflux/repository/NeuralNetMessageRepository.java @@ -0,0 +1,10 @@ +package com.example.mindflux.repository; +import com.example.mindflux.model.NeuralNetMessage; +import org.springframework.data.jpa.repository.*; +import org.springframework.data.repository.query.Param; +import java.util.*; +public interface NeuralNetMessageRepository extends JpaRepository { + @Query("select m from NeuralNetMessage m where (m.sender=:a and m.recipient=:b) or (m.sender=:b and m.recipient=:a) order by m.sentAt asc") + List conversation(@Param("a") String a, @Param("b") String b); + long countByRecipientAndReadAtIsNull(String recipient); +} diff --git a/src/main/java/com/example/mindflux/repository/NeuralNetPrivacyRepository.java b/src/main/java/com/example/mindflux/repository/NeuralNetPrivacyRepository.java new file mode 100644 index 0000000..100d531 --- /dev/null +++ b/src/main/java/com/example/mindflux/repository/NeuralNetPrivacyRepository.java @@ -0,0 +1,4 @@ +package com.example.mindflux.repository; +import com.example.mindflux.model.NeuralNetPrivacy; +import org.springframework.data.jpa.repository.JpaRepository; +public interface NeuralNetPrivacyRepository extends JpaRepository {} diff --git a/src/main/java/com/example/mindflux/service/NeuralNetService.java b/src/main/java/com/example/mindflux/service/NeuralNetService.java new file mode 100644 index 0000000..dbb95c4 --- /dev/null +++ b/src/main/java/com/example/mindflux/service/NeuralNetService.java @@ -0,0 +1,62 @@ +package com.example.mindflux.service; + +import com.example.mindflux.model.*; +import com.example.mindflux.repository.*; +import org.springframework.http.HttpStatus; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.web.server.ResponseStatusException; +import java.time.LocalDateTime; +import java.util.List; + +@Service +public class NeuralNetService { + private final NeuralNetPrivacyRepository privacy; + private final NeuralNetConnectionRepository connections; + private final NeuralNetMessageRepository messages; + private final UserRepository users; + public NeuralNetService(NeuralNetPrivacyRepository privacy, NeuralNetConnectionRepository connections, NeuralNetMessageRepository messages, UserRepository users) { + this.privacy=privacy; this.connections=connections; this.messages=messages; this.users=users; + } + public record Overview(String username, boolean enabled, List connections, List incoming, List outgoing, long unread) {} + public record ChatMessage(long id, String sender, String body, LocalDateTime sentAt) {} + private boolean enabled(String username) { return privacy.findById(username).map(NeuralNetPrivacy::isEnabled).orElse(false); } + private void requireEnabled(String username) { if (!enabled(username)) throw new ResponseStatusException(HttpStatus.FORBIDDEN, "NeuralNet ist für dieses Konto nicht aktiviert."); } + private void requireConnection(String a, String b) { if (connections.acceptedBetween(a,b).isEmpty()) throw new ResponseStatusException(HttpStatus.FORBIDDEN, "Private Nachrichten sind erst nach bestätigter Verbindung möglich."); } + public void enable(String username) { privacy.save(new NeuralNetPrivacy(username, true)); } + @Transactional(readOnly = true) + public Overview overview(String username) { + if (!enabled(username)) return new Overview(username, false, List.of(), List.of(), List.of(), 0); + List accepted=connections.acceptedFor(username).stream().map(c -> c.getRequester().equals(username) ? c.getAddressee() : c.getRequester()).toList(); + List incoming=connections.findByAddresseeAndStatusOrderByCreatedAtAsc(username,"PENDING").stream().map(NeuralNetConnection::getRequester).toList(); + List outgoing=connections.findByRequesterAndStatusOrderByCreatedAtAsc(username,"PENDING").stream().map(NeuralNetConnection::getAddressee).toList(); + return new Overview(username, true, accepted, incoming, outgoing, messages.countByRecipientAndReadAtIsNull(username)); + } + public void request(String username, String target) { + requireEnabled(username); target=cleanUser(target); + var targetUser=users.findByUsernameIgnoreCase(target).orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND, "Dieses NeuralNet-Mitglied ist nicht verfügbar.")); + target=targetUser.getUsername(); + if (username.equalsIgnoreCase(target) || !enabled(target)) throw new ResponseStatusException(HttpStatus.NOT_FOUND, "Dieses NeuralNet-Mitglied ist nicht verfügbar."); + if (connections.findByRequesterAndAddressee(username,target).isPresent() || connections.findByRequesterAndAddressee(target,username).isPresent()) throw new ResponseStatusException(HttpStatus.CONFLICT, "Zwischen euch besteht bereits eine Anfrage oder Verbindung."); + connections.save(new NeuralNetConnection(username,target)); + } + public void accept(String username, String requester) { var c=pendingFor(username,requester); c.setStatus("ACCEPTED"); c.setRespondedAt(LocalDateTime.now()); connections.save(c); } + public void decline(String username, String requester) { connections.delete(pendingFor(username,requester)); } + public void cancel(String username, String target) { connections.delete(connections.findByRequesterAndAddressee(username,cleanUser(target)).filter(c -> c.getStatus().equals("PENDING")).orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND, "Anfrage nicht gefunden."))); } + @Transactional + public List conversation(String username, String other) { + requireEnabled(username); other=cleanUser(other); requireConnection(username,other); + var result=messages.conversation(username,other); + result.stream().filter(m -> m.getRecipient().equals(username) && m.getReadAt()==null).forEach(m -> m.setReadAt(LocalDateTime.now())); + return result.stream().map(m -> new ChatMessage(m.getId(),m.getSender(),m.getBody(),m.getSentAt())).toList(); + } + public ChatMessage send(String username, String recipient, String body) { + requireEnabled(username); recipient=cleanUser(recipient); requireConnection(username,recipient); + String text=body==null ? "" : body.trim(); + if (text.isBlank() || text.length()>2000) throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "Eine Nachricht benötigt 1 bis 2000 Zeichen."); + var m=messages.save(new NeuralNetMessage(username,recipient,text)); + return new ChatMessage(m.getId(),m.getSender(),m.getBody(),m.getSentAt()); + } + private NeuralNetConnection pendingFor(String addressee,String requester) { return connections.findByRequesterAndAddressee(cleanUser(requester),addressee).filter(c -> c.getStatus().equals("PENDING")).orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND,"Anfrage nicht gefunden.")); } + private String cleanUser(String username) { if (username==null || username.trim().isBlank() || username.trim().length()>100) throw new ResponseStatusException(HttpStatus.BAD_REQUEST,"Ungültiger Benutzername."); return username.trim(); } +} diff --git a/src/main/resources/db/migration/V16__neuralnet.sql b/src/main/resources/db/migration/V16__neuralnet.sql new file mode 100644 index 0000000..414d20f --- /dev/null +++ b/src/main/resources/db/migration/V16__neuralnet.sql @@ -0,0 +1,28 @@ +CREATE TABLE neuralnet_privacy ( + username VARCHAR(100) PRIMARY KEY REFERENCES users(username) ON DELETE CASCADE ON UPDATE CASCADE, + enabled BOOLEAN NOT NULL DEFAULT FALSE +); + +CREATE TABLE neuralnet_connections ( + id BIGINT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY, + requester VARCHAR(100) NOT NULL REFERENCES users(username) ON DELETE CASCADE ON UPDATE CASCADE, + addressee VARCHAR(100) NOT NULL REFERENCES users(username) ON DELETE CASCADE ON UPDATE CASCADE, + status VARCHAR(20) NOT NULL DEFAULT 'PENDING', + created_at TIMESTAMP NOT NULL, + responded_at TIMESTAMP, + CONSTRAINT uq_neuralnet_connection UNIQUE (requester, addressee), + CONSTRAINT chk_neuralnet_no_self CHECK (requester <> addressee) +); +CREATE INDEX idx_neuralnet_connection_addressee ON neuralnet_connections(addressee, status); + +CREATE TABLE neuralnet_messages ( + id BIGINT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY, + sender VARCHAR(100) NOT NULL REFERENCES users(username) ON DELETE CASCADE ON UPDATE CASCADE, + recipient VARCHAR(100) NOT NULL REFERENCES users(username) ON DELETE CASCADE ON UPDATE CASCADE, + body VARCHAR(2000) NOT NULL, + sent_at TIMESTAMP NOT NULL, + read_at TIMESTAMP, + reported BOOLEAN NOT NULL DEFAULT FALSE +); +CREATE INDEX idx_neuralnet_message_pair ON neuralnet_messages(sender, recipient, sent_at); +CREATE INDEX idx_neuralnet_message_unread ON neuralnet_messages(recipient, read_at); diff --git a/src/main/resources/static/css/neuralnet.css b/src/main/resources/static/css/neuralnet.css new file mode 100644 index 0000000..ace6164 --- /dev/null +++ b/src/main/resources/static/css/neuralnet.css @@ -0,0 +1 @@ +.nn-head{margin:1.3rem 0}.nn-head>span,.nn-chat small{color:var(--accent);font:700 .72rem/1 monospace;letter-spacing:.14em}.nn-head h1{margin:.4rem 0;color:var(--heading)}.nn-head p{color:var(--muted)}.nn-optin{max-width:700px}.nn-optin p{color:var(--muted);line-height:1.6}.nn-app{display:grid;grid-template-columns:minmax(270px,340px) 1fr;gap:1rem}.nn-sidebar{display:grid;align-content:start;gap:1rem}.nn-request div{display:flex;gap:.45rem}.nn-request .mf-input{min-width:0}.nn-section{border-top:1px solid var(--border);padding-top:.85rem}.nn-section h2{font-size:.82rem;color:var(--heading);margin:0 0 .5rem}.nn-list{display:grid;gap:.45rem}.nn-item{display:flex;align-items:center;justify-content:space-between;gap:.5rem;border:1px solid var(--border);background:var(--surface-2);padding:.55rem .65rem;border-radius:8px;color:var(--text);font-size:.84rem}.nn-item button{border:0;background:none;color:var(--secondary);font:inherit;cursor:pointer}.nn-item button:hover,.nn-item button.active{color:var(--accent)}.nn-actions{display:flex;gap:.35rem}.nn-actions button{border:1px solid var(--border);padding:.22rem .38rem;border-radius:5px}.nn-chat{min-height:530px}.nn-empty{height:100%;min-height:430px;display:grid;place-content:center;text-align:center;gap:.5rem;color:var(--muted)}.nn-empty strong{color:var(--heading);font-size:1.1rem}.nn-chat header{border-bottom:1px solid var(--border);padding-bottom:.9rem}.nn-chat h2{margin:.25rem 0 0;color:var(--heading)}.nn-messages{display:flex;flex-direction:column;gap:.6rem;height:380px;overflow:auto;padding:1rem 0}.nn-message{align-self:flex-start;max-width:min(80%,550px);padding:.65rem .8rem;border-radius:10px;background:var(--surface-2);border:1px solid var(--border);color:var(--text);line-height:1.45}.nn-message.mine{align-self:flex-end;border-color:var(--border-accent);background:color-mix(in srgb,var(--accent) 12%,var(--surface))}.nn-message small{display:block;margin-top:.35rem;color:var(--muted);font-size:.66rem}.nn-compose{display:grid;grid-template-columns:1fr auto;gap:.6rem;border-top:1px solid var(--border);padding-top:.85rem}.nn-compose textarea{min-height:52px;resize:vertical;background:var(--surface-2);color:var(--text);border:1px solid var(--border);border-radius:8px;padding:.65rem;font:inherit}.sr-only{position:absolute;width:1px;height:1px;overflow:hidden;clip:rect(0,0,0,0)}@media(max-width:800px){.nn-app{grid-template-columns:1fr}.nn-chat{min-height:400px}.nn-messages{height:300px}.nn-request div{flex-wrap:wrap}.nn-request .mf-input{flex:1}} diff --git a/src/main/resources/static/js/neuralnet.js b/src/main/resources/static/js/neuralnet.js new file mode 100644 index 0000000..5723c91 --- /dev/null +++ b/src/main/resources/static/js/neuralnet.js @@ -0,0 +1 @@ +(function(){'use strict';var selected=null,me='',poll=null;function q(s){return document.querySelector(s)}function esc(s){return String(s).replace(/[&<>"']/g,c=>({'&':'&','<':'<','>':'>','"':'"',"'":'''}[c]))}async function api(url,method,body){var r=await fetch('/api/neuralnet'+url,{method:method||'GET',headers:{'Content-Type':'application/json'},body:body?JSON.stringify(body):undefined});if(!r.ok){var e=await r.json().catch(()=>({message:'Anfrage fehlgeschlagen.'}));throw new Error(e.message)}return r.status===204?null:r.json()}function item(name,actions){return '
'+esc(name)+''+actions+'
'}function render(data){q('[data-nn-incoming]').innerHTML=data.incoming.length?data.incoming.map(n=>item(n,'')).join(''):'Keine offenen Anfragen.';q('[data-nn-outgoing]').innerHTML=data.outgoing.length?data.outgoing.map(n=>item(n,'')).join(''):'Keine gesendeten Anfragen.';q('[data-nn-connections]').innerHTML=data.connections.length?data.connections.map(n=>item(n,'')).join(''):'Noch keine Verbindungen.'}async function refresh(){var data=await api('/overview');me=data.username;q('#nnOptIn').hidden=data.enabled;q('#nnApp').hidden=!data.enabled;if(data.enabled)render(data)}async function loadChat(){if(!selected)return;var rows=await api('/messages/'+encodeURIComponent(selected));q('[data-nn-empty]').hidden=true;q('[data-nn-conversation]').hidden=false;q('[data-nn-partner]').textContent=selected;q('[data-nn-messages]').innerHTML=rows.map(m=>'
'+esc(m.body)+''+esc(m.sender)+' · '+new Date(m.sentAt).toLocaleString('de-DE',{dateStyle:'short',timeStyle:'short'})+'
').join('');var box=q('[data-nn-messages]');box.scrollTop=box.scrollHeight}function message(text){var old=q('.nn-head p');old.textContent=text;setTimeout(()=>old.textContent='Erst eine bestätigte Anfrage, dann ein geschützter Nachrichtenkanal.',3500)}function init(){q('[data-nn-enable]').onclick=async()=>{await api('/enable','POST');refresh()};q('[data-nn-request]').onsubmit=async e=>{e.preventDefault();try{await api('/requests','POST',{username:q('#nn-user').value});q('#nn-user').value='';message('Kontaktanfrage gesendet.')}catch(x){message(x.message)}refresh()};q('.nn-sidebar').onclick=async e=>{var b=e.target.closest('[data-action]');if(!b)return;var u=b.dataset.user,a=b.dataset.action;try{if(a==='chat'){selected=u;await refresh();await loadChat();return}if(a==='accept')await api('/requests/'+encodeURIComponent(u)+'/accept','POST');if(a==='decline')await api('/requests/'+encodeURIComponent(u)+'/decline','POST');if(a==='cancel')await api('/requests/'+encodeURIComponent(u),'DELETE');await refresh()}catch(x){message(x.message)}};q('[data-nn-compose]').onsubmit=async e=>{e.preventDefault();try{await api('/messages','POST',{recipient:selected,body:q('#nn-message').value});q('#nn-message').value='';loadChat()}catch(x){message(x.message)}};refresh().catch(x=>message(x.message));poll=setInterval(()=>{refresh();if(selected)loadChat()},5000)}if(document.readyState==='loading')document.addEventListener('DOMContentLoaded',init);else init()})(); diff --git a/src/main/resources/templates/dashboard.html b/src/main/resources/templates/dashboard.html index cb93a74..d4c27d2 100644 --- a/src/main/resources/templates/dashboard.html +++ b/src/main/resources/templates/dashboard.html @@ -64,7 +64,7 @@ - +
diff --git a/src/main/resources/templates/soziales.html b/src/main/resources/templates/soziales.html index 9cb8d5e..804744a 100644 --- a/src/main/resources/templates/soziales.html +++ b/src/main/resources/templates/soziales.html @@ -1,51 +1,30 @@ - - - SysnaptiK Neural Soziales - - - + + NeuralNet – SysnaptiK Neural + + -
- -
- - - - - -
-

„Straßen? Wo wir hingehen, brauchen wir keine Straßen!“ 🚀

-

Das soziale Netzwerk für SysnaptiK Neural-User befindet sich noch in Entwicklung.
- Bald kannst du dich hier mit anderen Neural-Mitgliedern verbinden!

-
- 🚗💨💥 -
-

Zurück in die Zukunft... und bald auch ins SysnaptiK Neural-Netzwerk!

-
- -
- - - - - +
+ + +
NEURALNET

Private Lernverbindungen

Erst eine bestätigte Anfrage, dann ein geschützter Nachrichtenkanal.

+ + +
+ + diff --git a/src/test/java/com/example/mindflux/service/NeuralNetServiceTest.java b/src/test/java/com/example/mindflux/service/NeuralNetServiceTest.java new file mode 100644 index 0000000..8bbfb6a --- /dev/null +++ b/src/test/java/com/example/mindflux/service/NeuralNetServiceTest.java @@ -0,0 +1,28 @@ +package com.example.mindflux.service; + +import com.example.mindflux.model.NeuralNetPrivacy; +import com.example.mindflux.repository.*; +import org.junit.jupiter.api.Test; +import org.springframework.web.server.ResponseStatusException; +import java.util.Optional; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.mockito.Mockito.*; + +class NeuralNetServiceTest { + @Test + void rejectsMessagesBeforeTheConnectionIsAccepted() { + var privacy = mock(NeuralNetPrivacyRepository.class); + var connections = mock(NeuralNetConnectionRepository.class); + var messages = mock(NeuralNetMessageRepository.class); + var users = mock(UserRepository.class); + when(privacy.findById("alice")).thenReturn(Optional.of(new NeuralNetPrivacy("alice", true))); + when(connections.acceptedBetween("alice", "bob")).thenReturn(Optional.empty()); + + var service = new NeuralNetService(privacy, connections, messages, users); + + assertThatThrownBy(() -> service.send("alice", "bob", "Hallo")) + .isInstanceOf(ResponseStatusException.class) + .hasMessageContaining("bestätigter Verbindung"); + verify(messages, never()).save(any()); + } +}