diff --git a/.gitignore b/.gitignore
index 1b6985c..da56994 100644
--- a/.gitignore
+++ b/.gitignore
@@ -3,3 +3,4 @@
# Ignore Gradle build output directory
build
+.aider*
diff --git a/bin/main/application-dev.properties b/bin/main/application-dev.properties
new file mode 100644
index 0000000..94b18e0
--- /dev/null
+++ b/bin/main/application-dev.properties
@@ -0,0 +1,9 @@
+# Lokale Entwicklung – überschreibt application.properties
+
+# Cookies ohne Secure-Flag, da lokal kein HTTPS läuft
+app.cookie.secure=false
+
+# Klartext-Credentials für lokale DB (kein Umgebungsvariablen-Zwang)
+spring.mail.username=local@dev.invalid
+spring.mail.password=unused
+jwt.keystore.password=XUR!Rv&f$j3UsqD&
diff --git a/bin/main/application.properties b/bin/main/application.properties
index af966fe..9861494 100644
--- a/bin/main/application.properties
+++ b/bin/main/application.properties
@@ -22,18 +22,19 @@ spring.jpa.properties.hibernate.type.preferred_uuid_jdbc_type=VARCHAR
# Mailpit
spring.mail.host=smtp-relay.brevo.com
spring.mail.port=587
-spring.mail.username=a6b17a001@smtp-brevo.com
-spring.mail.password=xsmtpsib-77b691d562154574133d12b09d44a06e166d30091aac6642480771a0ae463a79-8yH3jHOd4nMMAwuS
+spring.mail.username=${MAIL_USERNAME}
+spring.mail.password=${MAIL_PASSWORD}
spring.mail.properties.mail.smtp.auth=true
spring.mail.properties.mail.smtp.starttls.enable=true
# JWT Keystore
jwt.keystore.path=classpath:xxx.jks
-jwt.keystore.password=${JWT_KEYSTORE_PASSWORD:XUR!Rv&f$j3UsqD&}
+jwt.keystore.password=${JWT_KEYSTORE_PASSWORD}
jwt.keystore.alias=xxx
# App
app.base-url=http://localhost:8080
+app.cookie.secure=true
# Theme – alle Farben hier ändern, Email-Style passt sich automatisch an
app.theme.color-bg=#1a1a2e
diff --git a/bin/main/de/oaa/xxx/config/CookieFactory.class b/bin/main/de/oaa/xxx/config/CookieFactory.class
new file mode 100644
index 0000000..0a4282b
Binary files /dev/null and b/bin/main/de/oaa/xxx/config/CookieFactory.class differ
diff --git a/bin/main/de/oaa/xxx/config/JwtFilter.class b/bin/main/de/oaa/xxx/config/JwtFilter.class
index 8bc0e48..5960bf6 100644
Binary files a/bin/main/de/oaa/xxx/config/JwtFilter.class and b/bin/main/de/oaa/xxx/config/JwtFilter.class differ
diff --git a/bin/main/de/oaa/xxx/config/JwtService.class b/bin/main/de/oaa/xxx/config/JwtService.class
index c2b4a84..0773f13 100644
Binary files a/bin/main/de/oaa/xxx/config/JwtService.class and b/bin/main/de/oaa/xxx/config/JwtService.class differ
diff --git a/bin/main/de/oaa/xxx/config/RateLimitFilter$Window.class b/bin/main/de/oaa/xxx/config/RateLimitFilter$Window.class
new file mode 100644
index 0000000..8927b01
Binary files /dev/null and b/bin/main/de/oaa/xxx/config/RateLimitFilter$Window.class differ
diff --git a/bin/main/de/oaa/xxx/config/RateLimitFilter.class b/bin/main/de/oaa/xxx/config/RateLimitFilter.class
new file mode 100644
index 0000000..3df2c8a
Binary files /dev/null and b/bin/main/de/oaa/xxx/config/RateLimitFilter.class differ
diff --git a/bin/main/de/oaa/xxx/config/SecurityConfig.class b/bin/main/de/oaa/xxx/config/SecurityConfig.class
index 34ffe48..7e1ab23 100644
Binary files a/bin/main/de/oaa/xxx/config/SecurityConfig.class and b/bin/main/de/oaa/xxx/config/SecurityConfig.class differ
diff --git a/bin/main/de/oaa/xxx/config/TokenBlacklistService.class b/bin/main/de/oaa/xxx/config/TokenBlacklistService.class
new file mode 100644
index 0000000..dfbac23
Binary files /dev/null and b/bin/main/de/oaa/xxx/config/TokenBlacklistService.class differ
diff --git a/bin/main/de/oaa/xxx/emailchange/EmailChangeController$EmailChangeRequest.class b/bin/main/de/oaa/xxx/emailchange/EmailChangeController$EmailChangeRequest.class
index bbbdc94..61a4d7c 100644
Binary files a/bin/main/de/oaa/xxx/emailchange/EmailChangeController$EmailChangeRequest.class and b/bin/main/de/oaa/xxx/emailchange/EmailChangeController$EmailChangeRequest.class differ
diff --git a/bin/main/de/oaa/xxx/emailchange/EmailChangeController.class b/bin/main/de/oaa/xxx/emailchange/EmailChangeController.class
index 3acc18f..01ea370 100644
Binary files a/bin/main/de/oaa/xxx/emailchange/EmailChangeController.class and b/bin/main/de/oaa/xxx/emailchange/EmailChangeController.class differ
diff --git a/bin/main/de/oaa/xxx/games/chastity/cardlock/CardLockController$AssignTaskRequest.class b/bin/main/de/oaa/xxx/games/chastity/cardlock/CardLockController$AssignTaskRequest.class
index 1d38ada..ae9220c 100644
Binary files a/bin/main/de/oaa/xxx/games/chastity/cardlock/CardLockController$AssignTaskRequest.class and b/bin/main/de/oaa/xxx/games/chastity/cardlock/CardLockController$AssignTaskRequest.class differ
diff --git a/bin/main/de/oaa/xxx/games/chastity/cardlock/CardLockController$FreezeRequest.class b/bin/main/de/oaa/xxx/games/chastity/cardlock/CardLockController$FreezeRequest.class
index 3376133..935af10 100644
Binary files a/bin/main/de/oaa/xxx/games/chastity/cardlock/CardLockController$FreezeRequest.class and b/bin/main/de/oaa/xxx/games/chastity/cardlock/CardLockController$FreezeRequest.class differ
diff --git a/bin/main/de/oaa/xxx/games/chastity/cardlock/CardLockController$ModifyCardsRequest.class b/bin/main/de/oaa/xxx/games/chastity/cardlock/CardLockController$ModifyCardsRequest.class
index 118f39c..36dee6b 100644
Binary files a/bin/main/de/oaa/xxx/games/chastity/cardlock/CardLockController$ModifyCardsRequest.class and b/bin/main/de/oaa/xxx/games/chastity/cardlock/CardLockController$ModifyCardsRequest.class differ
diff --git a/bin/main/de/oaa/xxx/games/chastity/cardlock/CardLockController.class b/bin/main/de/oaa/xxx/games/chastity/cardlock/CardLockController.class
index 4c42648..36fec47 100644
Binary files a/bin/main/de/oaa/xxx/games/chastity/cardlock/CardLockController.class and b/bin/main/de/oaa/xxx/games/chastity/cardlock/CardLockController.class differ
diff --git a/bin/main/de/oaa/xxx/games/chastity/timelock/TimeLockController$FreezeRequest.class b/bin/main/de/oaa/xxx/games/chastity/timelock/TimeLockController$FreezeRequest.class
index 528920f..c1ceaa8 100644
Binary files a/bin/main/de/oaa/xxx/games/chastity/timelock/TimeLockController$FreezeRequest.class and b/bin/main/de/oaa/xxx/games/chastity/timelock/TimeLockController$FreezeRequest.class differ
diff --git a/bin/main/de/oaa/xxx/games/chastity/timelock/TimeLockController.class b/bin/main/de/oaa/xxx/games/chastity/timelock/TimeLockController.class
index e2ff41d..3c29255 100644
Binary files a/bin/main/de/oaa/xxx/games/chastity/timelock/TimeLockController.class and b/bin/main/de/oaa/xxx/games/chastity/timelock/TimeLockController.class differ
diff --git a/bin/main/de/oaa/xxx/games/chastity/ttlock/TTLockTest.class b/bin/main/de/oaa/xxx/games/chastity/ttlock/TTLockTest.class
deleted file mode 100644
index d87240c..0000000
Binary files a/bin/main/de/oaa/xxx/games/chastity/ttlock/TTLockTest.class and /dev/null differ
diff --git a/bin/main/de/oaa/xxx/games/chastity/ttlock/unlocktypes b/bin/main/de/oaa/xxx/games/chastity/ttlock/unlocktypes
deleted file mode 100644
index 0d5b861..0000000
--- a/bin/main/de/oaa/xxx/games/chastity/ttlock/unlocktypes
+++ /dev/null
@@ -1,121 +0,0 @@
-1-unlock by app
-
-4-unlock by passcode
-
-5-Rise the lock (for parking lock)
-
-6-Lower the lock (for parking lock)
-
-7-unlock by IC card
-
-8-unlock by fingerprint
-
-9-unlock by wrist strap
-
-10-unlock by Mechanical key
-
-11-lock by app
-
-12-unlock by gateway
-
-29-apply some force on the Lock
-
-30-Door sensor closed
-
-31-Door sensor open
-
-32-open from inside
-
-33-lock by fingerprint
-
-34-lock by passcode
-
-35-lock by IC card
-
-36-lock by Mechanical key
-
-37-Use APP button to control the lock (rise, fall, stop, lock), mostly used for roller shutter door
-
-42-received new local mail
-
-43-received new other cities' mail
-
-44-Tamper alert
-
-45-Auto Lock
-
-46-unlock by unlock key
-
-47-lock by lock key
-
-48-System locked ( Caused by, for example: Using INVALID Passcode/Fingerprint/Card several times)
-
-49-unlock by hotel card
-
-50-Unlocked due to the high temperature
-
-51-Try to unlock with a deleted card
-
-52-Dead lock with APP
-
-53-Dead lock with passcode
-
-54-The car left (for parking lock)
-
-55-Use remote control lock or unlock lock
-
-57-Unlock with QR code success
-
-58-Unlock with QR code failed, it's expired
-
-59-Double locked
-
-60-Cancel double lock
-
-61-Lock with QR code success
-
-62-Lock with QR code failed, the lock is double locked
-
-63-Auto unlock at passage mode
-
-64-Door unclosed alarm
-
-65-Failed to unlock
-
-66-Failed to lock
-
-67-Face unlock success
-
-68-Face unlock failed - door locked from inside
-
-69-Lock with face
-
-71-Face unlock failed - expired or ineffective
-
-75-Unlocked by App granting
-
-76-Unlocked by remote granting
-
-77-Dual authentication Bluetooth unlock verification success, waiting for second user
-
-78-Dual authentication password unlock verification success, waiting for second user
-
-79-Dual authentication fingerprint unlock verification success, waiting for second user
-
-80-Dual authentication IC card unlock verification success, waiting for second user
-
-81-Dual authentication face card unlock verification success, waiting for second user
-
-82-Dual authentication wireless key unlock verification success, waiting for second user
-
-83-Dual authentication palm vein unlock verification success, waiting for second user
-
-84-Palm vein unlock success
-
-85-Palm vein unlock success
-
-86-Lock with palm vein
-
-88-Palm vein unlock failed - expired or ineffective
-
-92-Administrator password to unlock
\ No newline at end of file
diff --git a/bin/main/de/oaa/xxx/location/LocationEventController.class b/bin/main/de/oaa/xxx/location/LocationEventController.class
index 878a9e5..4af0952 100644
Binary files a/bin/main/de/oaa/xxx/location/LocationEventController.class and b/bin/main/de/oaa/xxx/location/LocationEventController.class differ
diff --git a/bin/main/de/oaa/xxx/user/LoginController$LoginRequest.class b/bin/main/de/oaa/xxx/user/LoginController$LoginRequest.class
index 3468070..7524117 100644
Binary files a/bin/main/de/oaa/xxx/user/LoginController$LoginRequest.class and b/bin/main/de/oaa/xxx/user/LoginController$LoginRequest.class differ
diff --git a/bin/main/de/oaa/xxx/user/LoginController.class b/bin/main/de/oaa/xxx/user/LoginController.class
index 80ae818..a360f35 100644
Binary files a/bin/main/de/oaa/xxx/user/LoginController.class and b/bin/main/de/oaa/xxx/user/LoginController.class differ
diff --git a/bin/main/de/oaa/xxx/user/User.class b/bin/main/de/oaa/xxx/user/User.class
index 53d9347..3a16bd4 100644
Binary files a/bin/main/de/oaa/xxx/user/User.class and b/bin/main/de/oaa/xxx/user/User.class differ
diff --git a/bin/main/de/oaa/xxx/user/UserController$BdsmDefaultsRequest.class b/bin/main/de/oaa/xxx/user/UserController$BdsmDefaultsRequest.class
index 9d01a7d..4eb5e2f 100644
Binary files a/bin/main/de/oaa/xxx/user/UserController$BdsmDefaultsRequest.class and b/bin/main/de/oaa/xxx/user/UserController$BdsmDefaultsRequest.class differ
diff --git a/bin/main/de/oaa/xxx/user/UserController$DatingFilterRequest.class b/bin/main/de/oaa/xxx/user/UserController$DatingFilterRequest.class
index a57b9d3..2cbf142 100644
Binary files a/bin/main/de/oaa/xxx/user/UserController$DatingFilterRequest.class and b/bin/main/de/oaa/xxx/user/UserController$DatingFilterRequest.class differ
diff --git a/bin/main/de/oaa/xxx/user/UserController$DatingRequest.class b/bin/main/de/oaa/xxx/user/UserController$DatingRequest.class
index 4f124bb..bfdaf90 100644
Binary files a/bin/main/de/oaa/xxx/user/UserController$DatingRequest.class and b/bin/main/de/oaa/xxx/user/UserController$DatingRequest.class differ
diff --git a/bin/main/de/oaa/xxx/user/UserController$GeburtsdatumChangeRequest.class b/bin/main/de/oaa/xxx/user/UserController$GeburtsdatumChangeRequest.class
index 6777983..029e8c3 100644
Binary files a/bin/main/de/oaa/xxx/user/UserController$GeburtsdatumChangeRequest.class and b/bin/main/de/oaa/xxx/user/UserController$GeburtsdatumChangeRequest.class differ
diff --git a/bin/main/de/oaa/xxx/user/UserController$LocationFilterRequest.class b/bin/main/de/oaa/xxx/user/UserController$LocationFilterRequest.class
index 6eb084c..312c4c6 100644
Binary files a/bin/main/de/oaa/xxx/user/UserController$LocationFilterRequest.class and b/bin/main/de/oaa/xxx/user/UserController$LocationFilterRequest.class differ
diff --git a/bin/main/de/oaa/xxx/user/UserController$NameChangeRequest.class b/bin/main/de/oaa/xxx/user/UserController$NameChangeRequest.class
index 2c3c89c..2f271ca 100644
Binary files a/bin/main/de/oaa/xxx/user/UserController$NameChangeRequest.class and b/bin/main/de/oaa/xxx/user/UserController$NameChangeRequest.class differ
diff --git a/bin/main/de/oaa/xxx/user/UserController$NewMemberDto.class b/bin/main/de/oaa/xxx/user/UserController$NewMemberDto.class
new file mode 100644
index 0000000..417cafd
Binary files /dev/null and b/bin/main/de/oaa/xxx/user/UserController$NewMemberDto.class differ
diff --git a/bin/main/de/oaa/xxx/user/UserController$NotificationPreferenceRequest.class b/bin/main/de/oaa/xxx/user/UserController$NotificationPreferenceRequest.class
index d9b778a..900a6af 100644
Binary files a/bin/main/de/oaa/xxx/user/UserController$NotificationPreferenceRequest.class and b/bin/main/de/oaa/xxx/user/UserController$NotificationPreferenceRequest.class differ
diff --git a/bin/main/de/oaa/xxx/user/UserController$PrivacyRequest.class b/bin/main/de/oaa/xxx/user/UserController$PrivacyRequest.class
index 8bb4a74..30fdcb5 100644
Binary files a/bin/main/de/oaa/xxx/user/UserController$PrivacyRequest.class and b/bin/main/de/oaa/xxx/user/UserController$PrivacyRequest.class differ
diff --git a/bin/main/de/oaa/xxx/user/UserController$ProfilePictureRequest.class b/bin/main/de/oaa/xxx/user/UserController$ProfilePictureRequest.class
index fa1b10b..5b88b83 100644
Binary files a/bin/main/de/oaa/xxx/user/UserController$ProfilePictureRequest.class and b/bin/main/de/oaa/xxx/user/UserController$ProfilePictureRequest.class differ
diff --git a/bin/main/de/oaa/xxx/user/UserController$ProfileRequest.class b/bin/main/de/oaa/xxx/user/UserController$ProfileRequest.class
index 78e399d..0b265b9 100644
Binary files a/bin/main/de/oaa/xxx/user/UserController$ProfileRequest.class and b/bin/main/de/oaa/xxx/user/UserController$ProfileRequest.class differ
diff --git a/bin/main/de/oaa/xxx/user/UserController$TtlockUserConfigDto.class b/bin/main/de/oaa/xxx/user/UserController$TtlockUserConfigDto.class
index 0cc079d..8af561b 100644
Binary files a/bin/main/de/oaa/xxx/user/UserController$TtlockUserConfigDto.class and b/bin/main/de/oaa/xxx/user/UserController$TtlockUserConfigDto.class differ
diff --git a/bin/main/de/oaa/xxx/user/UserController$TtlockUserConfigRequest.class b/bin/main/de/oaa/xxx/user/UserController$TtlockUserConfigRequest.class
index 3f66531..b68a227 100644
Binary files a/bin/main/de/oaa/xxx/user/UserController$TtlockUserConfigRequest.class and b/bin/main/de/oaa/xxx/user/UserController$TtlockUserConfigRequest.class differ
diff --git a/bin/main/de/oaa/xxx/user/UserController.class b/bin/main/de/oaa/xxx/user/UserController.class
index 5a7ab60..46c4cb7 100644
Binary files a/bin/main/de/oaa/xxx/user/UserController.class and b/bin/main/de/oaa/xxx/user/UserController.class differ
diff --git a/bin/main/de/oaa/xxx/user/UserEntity.class b/bin/main/de/oaa/xxx/user/UserEntity.class
index cb36ec9..a8df5b8 100644
Binary files a/bin/main/de/oaa/xxx/user/UserEntity.class and b/bin/main/de/oaa/xxx/user/UserEntity.class differ
diff --git a/bin/main/de/oaa/xxx/user/UserRepository.class b/bin/main/de/oaa/xxx/user/UserRepository.class
index 6caef78..b6adc79 100644
Binary files a/bin/main/de/oaa/xxx/user/UserRepository.class and b/bin/main/de/oaa/xxx/user/UserRepository.class differ
diff --git a/bin/main/de/oaa/xxx/user/UserService.class b/bin/main/de/oaa/xxx/user/UserService.class
index 917a588..67fb16b 100644
Binary files a/bin/main/de/oaa/xxx/user/UserService.class and b/bin/main/de/oaa/xxx/user/UserService.class differ
diff --git a/bin/main/static/community/feed.html b/bin/main/static/community/feed.html
index 8782278..5db8356 100644
--- a/bin/main/static/community/feed.html
+++ b/bin/main/static/community/feed.html
@@ -182,10 +182,17 @@
let composeBilderArr = [];
// ── Boot ──
- fetch('/login/me').then(r => r.ok ? r.json() : null).then(user => {
+ fetch('/login/me').then(r => r.ok ? r.json() : null).then(async user => {
if (user) {
myUserId = user.userId;
- loadFeed('mine');
+ const raw = sessionStorage.getItem('feedOpenPost');
+ if (raw) {
+ sessionStorage.removeItem('feedOpenPost');
+ loadFeed('mine');
+ openLbWithData(JSON.parse(raw));
+ } else {
+ await loadFeed('mine');
+ }
}
}).catch(() => {});
@@ -387,7 +394,7 @@
async function submitPost() {
const text = document.getElementById('composeText').value.trim();
- if (!text) return;
+ if (!text && composeBilderArr.length === 0) return;
const beitragTyp = document.querySelector('input[name="beitragTyp"]:checked').value;
const multiChoice = document.getElementById('multiChoice').checked;
const isPublic = document.getElementById('isPublic').checked;
@@ -495,6 +502,20 @@
document.getElementById('postLightbox').classList.add('open');
}
+ function openLbWithData(p) {
+ activeLbPostId = p.postId;
+ activeLbPostType = p.postType || 'FEED';
+ const tempDiv = document.createElement('div');
+ tempDiv.innerHTML = renderPostCard(p, 'mine');
+ const card = tempDiv.firstElementChild;
+ if (card) {
+ card.querySelectorAll('.post-actions').forEach(el => el.remove());
+ document.getElementById('lbPostBody').innerHTML = card.innerHTML;
+ }
+ loadLbComments(p.postId, p.postType || 'FEED');
+ document.getElementById('postLightbox').classList.add('open');
+ }
+
function closeLb() {
document.getElementById('postLightbox').classList.remove('open');
activeLbPostId = null;
diff --git a/bin/main/static/dating/besucher.html b/bin/main/static/dating/besucher.html
new file mode 100644
index 0000000..9435426
--- /dev/null
+++ b/bin/main/static/dating/besucher.html
@@ -0,0 +1,147 @@
+
+
+
+
+
+
+ Profilbesucher – xXx Sphere
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/main/resources/static/dating.html b/bin/main/static/dating/dating.html
similarity index 99%
rename from src/main/resources/static/dating.html
rename to bin/main/static/dating/dating.html
index ba95aa9..e4fbb13 100644
--- a/src/main/resources/static/dating.html
+++ b/bin/main/static/dating/dating.html
@@ -1038,7 +1038,7 @@
🎉
Keine weiteren Profile gefunden.
Schau später wieder vorbei!
-
+
Wird geladen…
@@ -1742,7 +1742,11 @@
document.getElementById('discoveryEmpty').style.display = 'none';
try {
const res = await fetch('/dating/discovery');
- if (res.status === 403) return;
+ if (res.status === 403) {
+ document.getElementById('discoveryLoading').style.display = 'none';
+ document.getElementById('discoveryEmpty').style.display = '';
+ return;
+ }
if (!res.ok) throw new Error();
discoveryQueue = await res.json();
discoveryIdx = 0;
diff --git a/bin/main/static/dating/likes.html b/bin/main/static/dating/likes.html
new file mode 100644
index 0000000..eab29e3
--- /dev/null
+++ b/bin/main/static/dating/likes.html
@@ -0,0 +1,168 @@
+
+
+
+
+
+
+
Likes – xXx Sphere
+
+
+
+
+
+
+
+
Likes
+
+
+ Premium – Mit Premium siehst du, wer dich geliket hat.
+
+
+
+
+
+
+
+
+
+
diff --git a/bin/main/static/dating/matches.html b/bin/main/static/dating/matches.html
new file mode 100644
index 0000000..b38002a
--- /dev/null
+++ b/bin/main/static/dating/matches.html
@@ -0,0 +1,139 @@
+
+
+
+
+
+
+
Matches – xXx Sphere
+
+
+
+
+
+
+
+
+
+
+
diff --git a/bin/main/static/games/chastity/neulock.html b/bin/main/static/games/chastity/neulock.html
index 2ff849c..8731ee3 100644
--- a/bin/main/static/games/chastity/neulock.html
+++ b/bin/main/static/games/chastity/neulock.html
@@ -314,6 +314,24 @@
+
+
+
+
+
🔒
+
Du bist bereits in einem Lock
+
+ Es ist bereits ein aktives Keuschheitslock für dein Konto vorhanden.
+ Beende oder öffne zuerst das bestehende Lock.
+
+
+
+
+
+
+
+
@@ -669,12 +687,29 @@
el.style.display = '';
el.scrollIntoView({ behavior: 'smooth', block: 'center' });
}
- function showActiveLockError() {
- const el = document.getElementById('errorMsg');
- el.innerHTML = 'Du befindest dich bereits in einem aktiven Lock. '
- + '
Zum aktiven Lock';
- el.style.display = '';
- el.scrollIntoView({ behavior: 'smooth', block: 'center' });
+ async function showActiveLockError() {
+ const modal = document.getElementById('activeLockModal');
+ const linkEl = document.getElementById('activeLockLink');
+ linkEl.style.display = 'none';
+
+ // Eigenes aktives Lock suchen (CardLock oder TimeLock)
+ try {
+ const [cardRes, timeRes] = await Promise.all([
+ fetch('/keyholder/mylock'),
+ fetch('/keyholder/timelock/mylock')
+ ]);
+ if (cardRes.status === 200) {
+ const d = await cardRes.json();
+ linkEl.href = '/games/chastity/activelock.html?lockId=' + d.lockId;
+ linkEl.style.display = '';
+ } else if (timeRes.status === 200) {
+ const d = await timeRes.json();
+ linkEl.href = '/games/chastity/activetimelock.html?lockId=' + d.lockId;
+ linkEl.style.display = '';
+ }
+ } catch (_) {}
+
+ modal.classList.add('open');
}
function setFieldError(rowId, msg) {
const row = document.getElementById(rowId);
diff --git a/bin/main/static/js/sidebar.js b/bin/main/static/js/sidebar.js
index 0a72fcd..e7cb4f1 100644
--- a/bin/main/static/js/sidebar.js
+++ b/bin/main/static/js/sidebar.js
@@ -80,9 +80,17 @@
...socialLinks.map(navLink),
].join('');
- const datingActive = path === '/dating.html';
- const datingCls = datingActive ? ' class="active"' : '';
- const datingItem = `
${I('DATING') || '♥'} Dating`;
+ const datingLinks = [
+ { href: '/dating/dating.html', icon: I('DATING') || '♥', label: 'Dating', id: 'navDating' },
+ { href: '/dating/besucher.html', icon: '👀', label: 'Besucher' },
+ { href: '/dating/likes.html', icon: '❤️', label: 'Likes' },
+ { href: '/dating/matches.html', icon: '💕', label: 'Matches' },
+ ];
+ const datingItem = datingLinks.map(({ href, icon, label, id }) => {
+ const cls = path === href ? ' class="active"' : '';
+ const idAttr = id ? ` id="${id}"` : '';
+ return `
${icon} ${label}`;
+ }).join('');
const fullHref = path + window.location.search;
const nav = groups.map(({ label, icon, items }) => {
@@ -152,6 +160,11 @@
document.querySelectorAll('.sidebar-group-toggle').forEach(toggle => {
toggle.addEventListener('click', e => {
e.preventDefault();
+ const href = toggle.getAttribute('href');
+ if (href && href !== '#') {
+ window.location.href = href;
+ return;
+ }
toggle.closest('.sidebar-group').classList.toggle('open');
});
});
@@ -230,7 +243,7 @@
const navDating = document.getElementById('navDating');
if (navDating) {
navDating.querySelector('a').href = user.datingAktiv
- ? '/dating.html'
+ ? '/dating/dating.html'
: '/konto/einstellungen.html#sec-dating';
}
diff --git a/bin/main/static/userhome.html b/bin/main/static/userhome.html
index c9ef4e2..fe81f6a 100644
--- a/bin/main/static/userhome.html
+++ b/bin/main/static/userhome.html
@@ -36,77 +36,63 @@
padding-bottom: 0.5rem;
border-bottom: 1px solid var(--color-secondary);
}
- .visitors-strip {
- display: flex; flex-wrap: wrap; gap: 0.75rem;
+ /* ── Aktivitäts-Grid (Besucher / Likes / Matches) ── */
+ .activity-grid {
+ display: grid;
+ grid-template-columns: repeat(3, 1fr);
+ gap: 1rem;
+ margin-bottom: 0.5rem;
}
- .visitor-card {
- display: flex; flex-direction: column; align-items: center; gap: 0.3rem;
- text-decoration: none; color: var(--color-text);
- width: 72px;
+ @media (max-width: 680px) {
+ .activity-grid { grid-template-columns: 1fr; }
}
- .visitor-card:hover .visitor-avatar { border-color: var(--color-primary); }
- .visitor-avatar {
- width: 56px; height: 56px; border-radius: 50%;
- background: var(--color-secondary);
- border: 2px solid var(--color-secondary);
+ .activity-col {
+ background: var(--color-card);
+ border: 1px solid var(--color-secondary);
+ border-radius: 12px;
+ padding: 0.75rem 0.85rem 0.85rem;
+ }
+ .activity-col-header {
+ display: flex; align-items: center; justify-content: space-between;
+ margin-bottom: 0.7rem;
+ }
+ .activity-col-title {
+ font-size: 0.78rem; font-weight: 700; color: var(--color-muted);
+ text-transform: uppercase; letter-spacing: 0.05em;
+ }
+ .activity-col-link {
+ font-size: 0.75rem; color: var(--color-primary);
+ text-decoration: none; font-weight: 600;
+ }
+ .activity-col-link:hover { text-decoration: underline; }
+ .activity-row {
+ display: flex; gap: 0.5rem;
+ }
+ /* Avatar-Karte */
+ .soc-card {
+ flex: 1; display: flex; flex-direction: column; align-items: center; gap: 0.3rem;
+ text-decoration: none; color: var(--color-text); cursor: pointer; min-width: 0;
+ }
+ .soc-card:hover .soc-avatar { border-color: var(--color-primary); }
+ .soc-avatar {
+ width: 48px; height: 48px; border-radius: 50%;
+ background: var(--color-secondary); border: 2px solid var(--color-secondary);
display: flex; align-items: center; justify-content: center;
- font-size: 1.4rem; overflow: hidden; flex-shrink: 0;
- transition: border-color 0.15s;
+ font-size: 1.2rem; overflow: hidden; flex-shrink: 0;
+ transition: border-color 0.15s; position: relative;
}
- .visitor-avatar img { width: 100%; height: 100%; object-fit: cover; border-radius: 50%; }
- .visitor-name {
- font-size: 0.75rem; text-align: center;
- white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
- width: 100%;
+ .soc-avatar img { width: 100%; height: 100%; object-fit: cover; border-radius: 50%; }
+ .soc-lock {
+ position: absolute; inset: 0;
+ display: flex; align-items: center; justify-content: center; font-size: 0.95rem;
}
- .visitor-time { font-size: 0.68rem; color: var(--color-muted); text-align: center; }
-
- /* ── Dating: Likes & Matches ── */
- .dating-strip { display: flex; flex-wrap: wrap; gap: 0.75rem; }
- .dating-card {
- display: flex; flex-direction: column; align-items: center; gap: 0.3rem;
- text-decoration: none; color: var(--color-text); width: 72px;
- }
- .dating-card:hover .dating-avatar { border-color: var(--color-primary); }
- .dating-avatar {
- width: 56px; height: 56px; border-radius: 50%;
- background: var(--color-secondary);
- border: 2px solid var(--color-secondary);
- display: flex; align-items: center; justify-content: center;
- font-size: 1.4rem; overflow: hidden; flex-shrink: 0;
- transition: border-color 0.15s;
- }
- .dating-avatar img { width: 100%; height: 100%; object-fit: cover; border-radius: 50%; }
- .dating-name {
- font-size: 0.75rem; text-align: center;
+ .soc-name {
+ font-size: 0.68rem; text-align: center;
white-space: nowrap; overflow: hidden; text-overflow: ellipsis; width: 100%;
}
- /* Verschwommene Karte für nicht-Premium */
- .dating-card-locked {
- width: 72px; display: flex; flex-direction: column; align-items: center; gap: 0.3rem;
- cursor: default;
- }
- .dating-avatar-blurred {
- width: 56px; height: 56px; border-radius: 50%;
- background: var(--color-secondary);
- border: 2px solid var(--color-secondary);
- overflow: hidden; flex-shrink: 0; position: relative;
- }
- .dating-avatar-blurred img {
- width: 100%; height: 100%; object-fit: cover; border-radius: 50%;
- filter: blur(6px); transform: scale(1.1);
- }
- .dating-avatar-blurred .lock-icon {
- position: absolute; inset: 0;
- display: flex; align-items: center; justify-content: center;
- font-size: 1.1rem;
- }
- .premium-hint {
- font-size: 0.65rem; color: var(--color-primary); text-align: center; font-weight: 600;
- }
- .match-badge {
- font-size: 0.65rem; color: var(--color-primary); text-align: center; font-weight: 600;
- }
+ .soc-sub { font-size: 0.62rem; color: var(--color-muted); text-align: center; }
+ .soc-sub-accent { font-size: 0.62rem; color: var(--color-primary); font-weight: 600; text-align: center; }
+ .activity-empty { font-size: 0.8rem; color: var(--color-muted); text-align: center; padding: 0.5rem 0; }
/* ── Location-Events ── */
.loc-event-list { display: flex; flex-direction: column; gap: 0.6rem; }
@@ -130,6 +116,159 @@
.loc-event-title { font-size: 0.92rem; font-weight: 600;
white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.loc-event-date { font-size: 0.75rem; color: var(--color-primary); margin-top: 0.15rem; }
+
+ /* ── Aktive Spiele ── */
+ .active-game-list { display: flex; flex-direction: column; gap: 0.6rem; }
+ .active-game-card {
+ display: flex; gap: 0.75rem; align-items: center;
+ background: var(--color-secondary); border: 1px solid var(--color-secondary);
+ border-radius: 10px; padding: 0.65rem 0.85rem;
+ text-decoration: none; color: inherit;
+ transition: border-color 0.15s;
+ }
+ .active-game-card:hover { border-color: var(--color-primary); }
+ .active-game-icon {
+ width: 48px; height: 48px; border-radius: 8px; flex-shrink: 0;
+ background: var(--color-card);
+ display: flex; align-items: center; justify-content: center; font-size: 1.6rem;
+ }
+ .active-game-body { flex: 1; min-width: 0; }
+ .active-game-title { font-size: 0.92rem; font-weight: 600; }
+ .active-game-sub { font-size: 0.75rem; color: var(--color-muted); margin-top: 0.1rem; }
+ .active-game-action {
+ font-size: 0.8rem; color: var(--color-primary); font-weight: 600; flex-shrink: 0;
+ }
+
+ /* ── Einladungen ── */
+ .invite-list { display: flex; flex-direction: column; gap: 0.6rem; }
+ .invite-card {
+ display: flex; gap: 0.75rem; align-items: center;
+ background: var(--color-secondary); border: 1px solid var(--color-secondary);
+ border-radius: 10px; padding: 0.65rem 0.85rem;
+ text-decoration: none; color: inherit;
+ transition: border-color 0.15s;
+ }
+ .invite-card:hover { border-color: var(--color-primary); }
+ .invite-avatar {
+ width: 40px; height: 40px; border-radius: 50%; flex-shrink: 0;
+ background: var(--color-card);
+ display: flex; align-items: center; justify-content: center; font-size: 1.1rem;
+ overflow: hidden;
+ }
+ .invite-avatar img { width: 100%; height: 100%; object-fit: cover; border-radius: 50%; }
+ .invite-body { flex: 1; min-width: 0; }
+ .invite-from { font-size: 0.88rem; font-weight: 600;
+ white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
+ .invite-type { font-size: 0.73rem; color: var(--color-muted); margin-top: 0.1rem; }
+ .invite-action { font-size: 0.8rem; color: var(--color-primary); font-weight: 600; flex-shrink: 0; }
+
+ /* ── Freundschaftsanfragen ── */
+ .friend-req-strip { display: flex; flex-wrap: wrap; gap: 0.75rem; }
+ .friend-req-card {
+ display: flex; flex-direction: column; align-items: center; gap: 0.3rem;
+ text-decoration: none; color: var(--color-text); width: 72px;
+ }
+ .friend-req-card:hover .friend-req-avatar { border-color: var(--color-primary); }
+ .friend-req-avatar {
+ width: 56px; height: 56px; border-radius: 50%;
+ background: var(--color-secondary);
+ border: 2px solid var(--color-primary);
+ display: flex; align-items: center; justify-content: center;
+ font-size: 1.4rem; overflow: hidden; flex-shrink: 0;
+ transition: border-color 0.15s;
+ }
+ .friend-req-avatar img { width: 100%; height: 100%; object-fit: cover; border-radius: 50%; }
+ .friend-req-name {
+ font-size: 0.75rem; text-align: center;
+ white-space: nowrap; overflow: hidden; text-overflow: ellipsis; width: 100%;
+ }
+ .friend-req-badge { font-size: 0.65rem; color: var(--color-primary); font-weight: 600; text-align: center; }
+
+ /* ── Compose ── */
+ .post-compose { background:var(--color-card); border:1px solid var(--color-secondary); border-radius:10px; padding:1rem; margin-bottom:1rem; transition:border-color 0.15s; }
+ .post-compose.drag-over { border-color:var(--color-primary); background:rgba(var(--color-primary-rgb,180,0,60),0.06); }
+ .compose-type { display:flex; gap:1.5rem; margin-bottom:0.75rem; }
+ .compose-type label { display:flex; align-items:center; gap:0.4rem; font-size:0.9rem; cursor:pointer; }
+ .post-compose textarea { width:100%; padding:0.6rem 0.85rem; border:1px solid var(--color-secondary); border-radius:6px; background:var(--color-secondary); color:var(--color-text); font-size:0.95rem; outline:none; transition:border-color 0.2s; resize:vertical; min-height:70px; box-sizing:border-box; }
+ .post-compose textarea:focus { border-color:var(--color-primary); }
+ .compose-thumbs { display:none; flex-wrap:wrap; gap:0.5rem; margin-top:0.5rem; }
+ .compose-thumb { position:relative; width:64px; height:64px; flex-shrink:0; }
+ .compose-thumb img { width:64px; height:64px; object-fit:cover; border-radius:6px; display:block; }
+ .compose-thumb-remove { position:absolute; top:-5px; right:-5px; background:rgba(0,0,0,0.7); border:none; color:#fff; border-radius:50%; font-size:0.65rem; cursor:pointer; display:flex; align-items:center; justify-content:center; padding:0; margin:0; width:auto; line-height:1; }
+ .umfrage-options { margin-top:0.5rem; }
+ .umfrage-option-row { display:flex; gap:0.5rem; margin-bottom:0.4rem; }
+ .umfrage-option-row input { flex:1; }
+ .umfrage-option-row button { width:auto; margin:0; padding:0.3rem 0.6rem; font-size:0.8rem; }
+ .compose-footer { display:flex; justify-content:space-between; align-items:center; margin-top:0.75rem; flex-wrap:wrap; gap:0.5rem; }
+ .multi-toggle { font-size:0.85rem; display:flex; align-items:center; gap:0.4rem; }
+ .privacy-toggle { font-size:0.85rem; display:flex; align-items:center; gap:0.4rem; }
+ .compose-action-btn { background:none; border:1px solid var(--color-secondary); color:var(--color-muted); border-radius:6px; padding:0.35rem 0.6rem; font-size:0.95rem; cursor:pointer; margin:0; width:auto; transition:border-color 0.15s,color 0.15s; }
+ .compose-action-btn:hover { border-color:var(--color-primary); color:var(--color-primary); background:none; }
+ label.compose-action-btn { display:inline-flex; align-items:center; }
+
+ /* ── Post Cards (1:1 wie Feed) ── */
+ .post-card { background:var(--color-card); border:1px solid var(--color-secondary); border-radius:10px; padding:1rem; margin-bottom:0.9rem; cursor:pointer; transition:border-color 0.15s; }
+ .post-card:hover { border-color:var(--color-primary); }
+ .post-header { display:flex; align-items:center; gap:0.7rem; margin-bottom:0.6rem; }
+ .post-avatar { width:36px; height:36px; border-radius:50%; background:var(--color-secondary); display:flex; align-items:center; justify-content:center; font-size:0.95rem; flex-shrink:0; overflow:hidden; }
+ .post-avatar img { width:100%; height:100%; object-fit:cover; }
+ .post-author { font-weight:600; font-size:0.9rem; }
+ .post-meta { font-size:0.75rem; color:var(--color-muted); }
+ .post-text { font-size:0.95rem; line-height:1.5; white-space:pre-wrap; word-break:break-word; }
+ .post-bild { width:100%; max-height:400px; object-fit:contain; border-radius:6px; margin-top:0.5rem; display:block; }
+ .post-actions { display:flex; gap:1rem; margin-top:0.75rem; align-items:center; flex-wrap:wrap; }
+ .post-action-btn { background:none; border:none; color:var(--color-muted); font-size:0.85rem; padding:0; display:flex; align-items:center; gap:0.3rem; margin:0; width:auto; pointer-events:none; }
+ .post-action-btn.active { color:var(--color-primary); }
+ .gruppe-badge { display:inline-flex; align-items:center; gap:0.3rem; font-size:0.75rem; color:var(--color-muted); background:var(--color-secondary); border-radius:4px; padding:0.15rem 0.45rem; margin-left:0.3rem; }
+ .umfrage-option-bar { margin:0.3rem 0; border-radius:6px; overflow:hidden; border:1px solid var(--color-secondary); position:relative; }
+ .umfrage-option-bar.voted { border-color:var(--color-primary); }
+ .umfrage-bar-fill { position:absolute; inset:0; background:rgba(var(--color-primary-rgb,180,0,60),0.15); }
+ .umfrage-bar-content { position:relative; display:flex; justify-content:space-between; padding:0.45rem 0.75rem; font-size:0.88rem; }
+ .umfrage-total { font-size:0.78rem; color:var(--color-muted); margin-top:0.3rem; }
+
+ /* ── Neue Mitglieder ── */
+ .new-members-strip {
+ display: flex;
+ gap: 0.75rem;
+ overflow-x: auto;
+ padding-bottom: 0.4rem;
+ scrollbar-width: thin;
+ scrollbar-color: var(--color-secondary) transparent;
+ }
+ .new-members-strip::-webkit-scrollbar { height: 4px; }
+ .new-members-strip::-webkit-scrollbar-thumb { background: var(--color-secondary); border-radius: 2px; }
+ .nm-card {
+ flex: 0 0 160px;
+ background: var(--color-card);
+ border: 1px solid var(--color-secondary);
+ border-radius: 12px;
+ overflow: hidden;
+ text-decoration: none;
+ color: var(--color-text);
+ display: flex;
+ flex-direction: column;
+ transition: border-color 0.15s, box-shadow 0.15s;
+ }
+ .nm-card:hover { border-color: var(--color-primary); box-shadow: 0 4px 18px rgba(0,0,0,0.35); }
+ .nm-card-img {
+ width: 100%; aspect-ratio: 1; flex-shrink: 0;
+ overflow: hidden; background: var(--color-secondary);
+ display: flex; align-items: center; justify-content: center;
+ font-size: 2.5rem; position: relative;
+ }
+ .nm-card-img img { width: 100%; height: 100%; object-fit: cover; display: block; }
+ .nm-card-body { padding: 0.6rem 0.65rem; display: flex; flex-direction: column; gap: 0.25rem; }
+ .nm-card-name { font-weight: 700; font-size: 0.9rem; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
+ .nm-card-meta { display: flex; flex-wrap: wrap; gap: 0.25rem; }
+ .nm-card-chip {
+ padding: 0.1rem 0.4rem; border-radius: 20px;
+ background: var(--color-secondary); font-size: 0.7rem; color: var(--color-muted);
+ }
+ .nm-card-desc {
+ font-size: 0.75rem; color: var(--color-muted); line-height: 1.35;
+ overflow: hidden; display: -webkit-box;
+ -webkit-line-clamp: 2; -webkit-box-orient: vertical;
+ }
@@ -138,22 +277,62 @@
Home
-
-
+
diff --git a/docker-compose.yml b/docker-compose.yml
index e4c89a7..53144ba 100644
--- a/docker-compose.yml
+++ b/docker-compose.yml
@@ -5,9 +5,11 @@ services:
restart: always
environment:
MYSQL_DATABASE: xxx_sphere
- MYSQL_ROOT_PASSWORD: xxxsphere123!
+ MYSQL_ROOT_PASSWORD: ${DB_ROOT_PASSWORD}
+ MYSQL_USER: ${DB_USER}
+ MYSQL_PASSWORD: ${DB_PASSWORD}
ports:
- - "3306:3306" # <--- Jetzt steht es korrekt alleine!
+ - "127.0.0.1:3306:3306"
volumes:
# Format: [Pfad auf dem Proxmox-Host]:[Pfad im Container]
- /mnt/pve_nas/.mysql_data:/var/lib/mysql
@@ -22,9 +24,11 @@ services:
environment:
# Wir biegen localhost auf den Service-Namen 'db' um
- SPRING_DATASOURCE_URL=jdbc:mysql://db:3306/xxx_sphere?useSSL=false&allowPublicKeyRetrieval=true&serverTimezone=UTC
- # Hier injizieren wir die Werte für deine Platzhalter
- - DB_USER=root
- - DB_PASSWORD=xxxsphere123!
+ - DB_USER=${DB_USER}
+ - DB_PASSWORD=${DB_PASSWORD}
+ - MAIL_USERNAME=${MAIL_USERNAME}
+ - MAIL_PASSWORD=${MAIL_PASSWORD}
+ - JWT_KEYSTORE_PASSWORD=${JWT_KEYSTORE_PASSWORD}
# Wartet kurz, bis die DB wirklich bereit ist (optional, aber empfohlen)
restart: on-failure
diff --git a/src/main/java/de/oaa/xxx/config/CookieFactory.java b/src/main/java/de/oaa/xxx/config/CookieFactory.java
new file mode 100644
index 0000000..1979dcd
--- /dev/null
+++ b/src/main/java/de/oaa/xxx/config/CookieFactory.java
@@ -0,0 +1,27 @@
+package de.oaa.xxx.config;
+
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.http.ResponseCookie;
+import org.springframework.stereotype.Component;
+
+import java.time.Duration;
+
+@Component
+public class CookieFactory {
+
+ private final boolean secure;
+
+ public CookieFactory(@Value("${app.cookie.secure:true}") boolean secure) {
+ this.secure = secure;
+ }
+
+ public ResponseCookie jwtCookie(String token, Duration maxAge) {
+ return ResponseCookie.from("jwt", token)
+ .httpOnly(true)
+ .secure(secure)
+ .sameSite("Strict")
+ .path("/")
+ .maxAge(maxAge)
+ .build();
+ }
+}
diff --git a/src/main/java/de/oaa/xxx/config/JwtFilter.java b/src/main/java/de/oaa/xxx/config/JwtFilter.java
index ab27a0c..c816975 100644
--- a/src/main/java/de/oaa/xxx/config/JwtFilter.java
+++ b/src/main/java/de/oaa/xxx/config/JwtFilter.java
@@ -18,9 +18,11 @@ import java.util.Collections;
public class JwtFilter extends OncePerRequestFilter {
private final JwtService jwtService;
+ private final TokenBlacklistService tokenBlacklist;
- public JwtFilter(JwtService jwtService) {
+ public JwtFilter(JwtService jwtService, TokenBlacklistService tokenBlacklist) {
this.jwtService = jwtService;
+ this.tokenBlacklist = tokenBlacklist;
}
@Override
@@ -32,10 +34,13 @@ public class JwtFilter extends OncePerRequestFilter {
if ("jwt".equals(cookie.getName())) {
try {
Claims claims = jwtService.validateAndGetClaims(cookie.getValue());
- UsernamePasswordAuthenticationToken auth = new UsernamePasswordAuthenticationToken(
- claims.getSubject(), null, Collections.emptyList()
- );
- SecurityContextHolder.getContext().setAuthentication(auth);
+ String jti = claims.getId();
+ if (jti == null || !tokenBlacklist.isBlacklisted(jti)) {
+ UsernamePasswordAuthenticationToken auth = new UsernamePasswordAuthenticationToken(
+ claims.getSubject(), null, Collections.emptyList()
+ );
+ SecurityContextHolder.getContext().setAuthentication(auth);
+ }
} catch (Exception e) {
// Ungültiger oder abgelaufener Token – ohne Authentifizierung weiter
}
diff --git a/src/main/java/de/oaa/xxx/config/JwtService.java b/src/main/java/de/oaa/xxx/config/JwtService.java
index 9ea02e9..c0d3d32 100644
--- a/src/main/java/de/oaa/xxx/config/JwtService.java
+++ b/src/main/java/de/oaa/xxx/config/JwtService.java
@@ -10,6 +10,7 @@ import java.security.KeyStore;
import java.security.PrivateKey;
import java.security.PublicKey;
import java.util.Date;
+import java.util.UUID;
@Service
public class JwtService {
@@ -33,12 +34,17 @@ public class JwtService {
return Jwts.builder()
.subject(email)
.claim("name", name)
+ .id(UUID.randomUUID().toString())
.issuedAt(new Date())
.expiration(new Date(System.currentTimeMillis() + EXPIRATION_MS))
.signWith(privateKey)
.compact();
}
+ public long getExpirationMs() {
+ return EXPIRATION_MS;
+ }
+
public Claims validateAndGetClaims(String token) {
return Jwts.parser()
.verifyWith(publicKey)
diff --git a/src/main/java/de/oaa/xxx/config/RateLimitFilter.java b/src/main/java/de/oaa/xxx/config/RateLimitFilter.java
new file mode 100644
index 0000000..89b03c4
--- /dev/null
+++ b/src/main/java/de/oaa/xxx/config/RateLimitFilter.java
@@ -0,0 +1,70 @@
+package de.oaa.xxx.config;
+
+import jakarta.servlet.FilterChain;
+import jakarta.servlet.ServletException;
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletResponse;
+import org.springframework.stereotype.Component;
+import org.springframework.web.filter.OncePerRequestFilter;
+
+import java.io.IOException;
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.atomic.AtomicInteger;
+
+@Component
+public class RateLimitFilter extends OncePerRequestFilter {
+
+ private static final int MAX_REQUESTS = 10;
+ private static final long WINDOW_MS = 60_000;
+
+ private static final String[] RATE_LIMITED_PATHS = {
+ "/login", "/registration", "/password-reset"
+ };
+
+ private record Window(AtomicInteger count, long startMs) {}
+ private final Map
windows = new ConcurrentHashMap<>();
+
+ @Override
+ protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain)
+ throws ServletException, IOException {
+ String path = request.getRequestURI();
+ boolean isRateLimited = false;
+ for (String p : RATE_LIMITED_PATHS) {
+ if (path.equals(p) || path.startsWith(p + "/")) {
+ isRateLimited = true;
+ break;
+ }
+ }
+
+ if (isRateLimited) {
+ String ip = getClientIp(request);
+ String key = ip + ":" + path;
+ long now = System.currentTimeMillis();
+
+ Window window = windows.compute(key, (k, w) -> {
+ if (w == null || now - w.startMs() > WINDOW_MS) {
+ return new Window(new AtomicInteger(1), now);
+ }
+ w.count().incrementAndGet();
+ return w;
+ });
+
+ if (window.count().get() > MAX_REQUESTS) {
+ response.setStatus(429);
+ response.getWriter().write("Too many requests");
+ return;
+ }
+ }
+
+ chain.doFilter(request, response);
+ }
+
+ private String getClientIp(HttpServletRequest request) {
+ String xff = request.getHeader("X-Forwarded-For");
+ if (xff != null && !xff.isBlank()) {
+ return xff.split(",")[0].trim();
+ }
+ return request.getRemoteAddr();
+ }
+}
diff --git a/src/main/java/de/oaa/xxx/config/SecurityConfig.java b/src/main/java/de/oaa/xxx/config/SecurityConfig.java
index 5f45d91..e6a2579 100644
--- a/src/main/java/de/oaa/xxx/config/SecurityConfig.java
+++ b/src/main/java/de/oaa/xxx/config/SecurityConfig.java
@@ -71,6 +71,10 @@ public class SecurityConfig {
.requestMatchers("/games/chastity/joinlock.html").authenticated()
.requestMatchers("/community/benachrichtigungen.html").authenticated()
.requestMatchers("/community/abonnements.html").authenticated()
+ .requestMatchers("/dating/dating.html").authenticated()
+ .requestMatchers("/dating/besucher.html").authenticated()
+ .requestMatchers("/dating/likes.html").authenticated()
+ .requestMatchers("/dating/matches.html").authenticated()
.requestMatchers("/community/locations.html").authenticated()
.requestMatchers("/community/location-detail.html").authenticated()
.requestMatchers("/community/events.html").authenticated()
@@ -80,7 +84,6 @@ public class SecurityConfig {
.requestMatchers("/notifications/**").authenticated()
.requestMatchers("/events/**").authenticated()
.requestMatchers("/*.html").permitAll()
- .requestMatchers("/**/*.html").permitAll()
.requestMatchers("/help/*.html").permitAll()
.requestMatchers("/css/**").permitAll()
.requestMatchers("/js/**").permitAll()
diff --git a/src/main/java/de/oaa/xxx/config/TokenBlacklistService.java b/src/main/java/de/oaa/xxx/config/TokenBlacklistService.java
new file mode 100644
index 0000000..f6901cd
--- /dev/null
+++ b/src/main/java/de/oaa/xxx/config/TokenBlacklistService.java
@@ -0,0 +1,34 @@
+package de.oaa.xxx.config;
+
+import org.springframework.scheduling.annotation.Scheduled;
+import org.springframework.stereotype.Service;
+
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
+
+@Service
+public class TokenBlacklistService {
+
+ // jti -> Ablaufzeit in ms
+ private final Map blacklist = new ConcurrentHashMap<>();
+
+ public void blacklist(String jti, long expiryMs) {
+ blacklist.put(jti, expiryMs);
+ }
+
+ public boolean isBlacklisted(String jti) {
+ Long expiry = blacklist.get(jti);
+ if (expiry == null) return false;
+ if (System.currentTimeMillis() > expiry) {
+ blacklist.remove(jti);
+ return false;
+ }
+ return true;
+ }
+
+ @Scheduled(fixedDelay = 3_600_000)
+ public void cleanup() {
+ long now = System.currentTimeMillis();
+ blacklist.entrySet().removeIf(e -> now > e.getValue());
+ }
+}
diff --git a/src/main/java/de/oaa/xxx/emailchange/EmailChangeController.java b/src/main/java/de/oaa/xxx/emailchange/EmailChangeController.java
index 6627313..b6ac423 100644
--- a/src/main/java/de/oaa/xxx/emailchange/EmailChangeController.java
+++ b/src/main/java/de/oaa/xxx/emailchange/EmailChangeController.java
@@ -1,23 +1,29 @@
package de.oaa.xxx.emailchange;
+import java.io.IOException;
+import java.security.Principal;
+import java.util.UUID;
+import java.util.regex.Pattern;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.http.HttpHeaders;
+import org.springframework.http.ResponseEntity;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.PathVariable;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.RequestBody;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+
+import de.oaa.xxx.config.CookieFactory;
import de.oaa.xxx.mail.Email;
import de.oaa.xxx.mail.MailService;
import de.oaa.xxx.mail.MailTemplateService;
import de.oaa.xxx.registration.RegistrationRepository;
import de.oaa.xxx.user.UserRepository;
import jakarta.servlet.http.HttpServletResponse;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-import org.springframework.beans.factory.annotation.Value;
-import org.springframework.http.HttpHeaders;
-import org.springframework.http.ResponseCookie;
-import org.springframework.http.ResponseEntity;
-import org.springframework.web.bind.annotation.*;
-
-import java.io.IOException;
-import java.security.Principal;
-import java.util.UUID;
-import java.util.regex.Pattern;
@RestController
@RequestMapping("/email-change")
@@ -34,17 +40,20 @@ public class EmailChangeController {
private final RegistrationRepository registrationRepository;
private final MailService mailService;
private final MailTemplateService mailTemplateService;
+ private final CookieFactory cookieFactory;
public EmailChangeController(EmailChangeRepository emailChangeRepository,
UserRepository userRepository,
RegistrationRepository registrationRepository,
MailService mailService,
- MailTemplateService mailTemplateService) {
+ MailTemplateService mailTemplateService,
+ CookieFactory cookieFactory) {
this.emailChangeRepository = emailChangeRepository;
this.userRepository = userRepository;
this.registrationRepository = registrationRepository;
this.mailService = mailService;
this.mailTemplateService = mailTemplateService;
+ this.cookieFactory = cookieFactory;
}
record EmailChangeRequest(String newEmail) {}
@@ -113,13 +122,7 @@ public class EmailChangeController {
emailChangeRepository.delete(entity.get());
// Clear JWT cookie so user must log in with new email
- ResponseCookie cookie = ResponseCookie.from("jwt", "")
- .httpOnly(true)
- .sameSite("Strict")
- .path("/")
- .maxAge(0)
- .build();
- response.addHeader(HttpHeaders.SET_COOKIE, cookie.toString());
+ response.addHeader(HttpHeaders.SET_COOKIE, cookieFactory.jwtCookie("", java.time.Duration.ZERO).toString());
response.sendRedirect("/login.html?emailChanged=1");
}
}
diff --git a/src/main/java/de/oaa/xxx/games/chastity/cardlock/CardLockController.java b/src/main/java/de/oaa/xxx/games/chastity/cardlock/CardLockController.java
index 6ec3630..189db52 100644
--- a/src/main/java/de/oaa/xxx/games/chastity/cardlock/CardLockController.java
+++ b/src/main/java/de/oaa/xxx/games/chastity/cardlock/CardLockController.java
@@ -264,7 +264,8 @@ public class CardLockController {
}
}
- return ResponseEntity.ok(Map.of("lockId", lock.getLockId().toString(), "unlockCode", lock.getUnlockCode(),
+ return ResponseEntity.ok(Map.of("lockId", lock.getLockId().toString(),
+ "unlockCode", lock.getUnlockCode() != null ? lock.getUnlockCode() : "",
"keyholderPending", keyholderPending));
}
@@ -408,6 +409,26 @@ public class CardLockController {
return ResponseEntity.ok(Map.of("lockId", activeLockId.get().toString()));
}
+ @DeleteMapping("/mylock")
+ @Transactional
+ public ResponseEntity deleteMyActiveLock(Principal principal) {
+ UUID myId = userService.requireUser(principal).getUserId();
+ var lockOpt = cardlockRepository.findByLockee(myId).stream()
+ .filter(l -> l.getStartTime() != null && l.getUnlockTime() == null)
+ .findFirst();
+ if (lockOpt.isEmpty())
+ return ResponseEntity.noContent().build();
+ var l = lockOpt.get();
+ CardLockService service = cardLockServiceFactory.create(l);
+ service.unlock(l.getUnlockCode());
+ var verifications = verificationRepository.findByLockId(l.getLockId());
+ verifications.forEach(v -> verificationVoteRepository.deleteAllByVerificationId(v.getDisplayId()));
+ verificationRepository.deleteAll(verifications);
+ invitationRepository.deleteByLockId(l.getLockId());
+ cardlockRepository.deleteById(l.getLockId());
+ return ResponseEntity.noContent().build();
+ }
+
@GetMapping("/cardlock/{lockId}")
public ResponseEntity
-
+
Wird geladen…
@@ -1742,7 +1742,11 @@
document.getElementById('discoveryEmpty').style.display = 'none';
try {
const res = await fetch('/dating/discovery');
- if (res.status === 403) return;
+ if (res.status === 403) {
+ document.getElementById('discoveryLoading').style.display = 'none';
+ document.getElementById('discoveryEmpty').style.display = '';
+ return;
+ }
if (!res.ok) throw new Error();
discoveryQueue = await res.json();
discoveryIdx = 0;
diff --git a/src/main/resources/static/dating/likes.html b/src/main/resources/static/dating/likes.html
new file mode 100644
index 0000000..eab29e3
--- /dev/null
+++ b/src/main/resources/static/dating/likes.html
@@ -0,0 +1,168 @@
+
+
+
+
+
+
+
Likes – xXx Sphere
+
+
+
+
+
+
+
+
Likes
+
+
+ Premium – Mit Premium siehst du, wer dich geliket hat.
+
+
+
+
+
+
+
+
+
+
diff --git a/src/main/resources/static/dating/matches.html b/src/main/resources/static/dating/matches.html
new file mode 100644
index 0000000..b38002a
--- /dev/null
+++ b/src/main/resources/static/dating/matches.html
@@ -0,0 +1,139 @@
+
+
+
+
+
+
+
Matches – xXx Sphere
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/main/resources/static/games/chastity/neulock.html b/src/main/resources/static/games/chastity/neulock.html
index 2ff849c..8731ee3 100644
--- a/src/main/resources/static/games/chastity/neulock.html
+++ b/src/main/resources/static/games/chastity/neulock.html
@@ -314,6 +314,24 @@
+
+
+
+
+
🔒
+
Du bist bereits in einem Lock
+
+ Es ist bereits ein aktives Keuschheitslock für dein Konto vorhanden.
+ Beende oder öffne zuerst das bestehende Lock.
+
+
+
+
+
+
+
+
@@ -669,12 +687,29 @@
el.style.display = '';
el.scrollIntoView({ behavior: 'smooth', block: 'center' });
}
- function showActiveLockError() {
- const el = document.getElementById('errorMsg');
- el.innerHTML = 'Du befindest dich bereits in einem aktiven Lock. '
- + '
Zum aktiven Lock';
- el.style.display = '';
- el.scrollIntoView({ behavior: 'smooth', block: 'center' });
+ async function showActiveLockError() {
+ const modal = document.getElementById('activeLockModal');
+ const linkEl = document.getElementById('activeLockLink');
+ linkEl.style.display = 'none';
+
+ // Eigenes aktives Lock suchen (CardLock oder TimeLock)
+ try {
+ const [cardRes, timeRes] = await Promise.all([
+ fetch('/keyholder/mylock'),
+ fetch('/keyholder/timelock/mylock')
+ ]);
+ if (cardRes.status === 200) {
+ const d = await cardRes.json();
+ linkEl.href = '/games/chastity/activelock.html?lockId=' + d.lockId;
+ linkEl.style.display = '';
+ } else if (timeRes.status === 200) {
+ const d = await timeRes.json();
+ linkEl.href = '/games/chastity/activetimelock.html?lockId=' + d.lockId;
+ linkEl.style.display = '';
+ }
+ } catch (_) {}
+
+ modal.classList.add('open');
}
function setFieldError(rowId, msg) {
const row = document.getElementById(rowId);
diff --git a/src/main/resources/static/js/sidebar.js b/src/main/resources/static/js/sidebar.js
index 0a72fcd..e7cb4f1 100644
--- a/src/main/resources/static/js/sidebar.js
+++ b/src/main/resources/static/js/sidebar.js
@@ -80,9 +80,17 @@
...socialLinks.map(navLink),
].join('');
- const datingActive = path === '/dating.html';
- const datingCls = datingActive ? ' class="active"' : '';
- const datingItem = `
${I('DATING') || '♥'} Dating`;
+ const datingLinks = [
+ { href: '/dating/dating.html', icon: I('DATING') || '♥', label: 'Dating', id: 'navDating' },
+ { href: '/dating/besucher.html', icon: '👀', label: 'Besucher' },
+ { href: '/dating/likes.html', icon: '❤️', label: 'Likes' },
+ { href: '/dating/matches.html', icon: '💕', label: 'Matches' },
+ ];
+ const datingItem = datingLinks.map(({ href, icon, label, id }) => {
+ const cls = path === href ? ' class="active"' : '';
+ const idAttr = id ? ` id="${id}"` : '';
+ return `
${icon} ${label}`;
+ }).join('');
const fullHref = path + window.location.search;
const nav = groups.map(({ label, icon, items }) => {
@@ -152,6 +160,11 @@
document.querySelectorAll('.sidebar-group-toggle').forEach(toggle => {
toggle.addEventListener('click', e => {
e.preventDefault();
+ const href = toggle.getAttribute('href');
+ if (href && href !== '#') {
+ window.location.href = href;
+ return;
+ }
toggle.closest('.sidebar-group').classList.toggle('open');
});
});
@@ -230,7 +243,7 @@
const navDating = document.getElementById('navDating');
if (navDating) {
navDating.querySelector('a').href = user.datingAktiv
- ? '/dating.html'
+ ? '/dating/dating.html'
: '/konto/einstellungen.html#sec-dating';
}
diff --git a/src/main/resources/static/userhome.html b/src/main/resources/static/userhome.html
index c9ef4e2..fe81f6a 100644
--- a/src/main/resources/static/userhome.html
+++ b/src/main/resources/static/userhome.html
@@ -36,77 +36,63 @@
padding-bottom: 0.5rem;
border-bottom: 1px solid var(--color-secondary);
}
- .visitors-strip {
- display: flex; flex-wrap: wrap; gap: 0.75rem;
+ /* ── Aktivitäts-Grid (Besucher / Likes / Matches) ── */
+ .activity-grid {
+ display: grid;
+ grid-template-columns: repeat(3, 1fr);
+ gap: 1rem;
+ margin-bottom: 0.5rem;
}
- .visitor-card {
- display: flex; flex-direction: column; align-items: center; gap: 0.3rem;
- text-decoration: none; color: var(--color-text);
- width: 72px;
+ @media (max-width: 680px) {
+ .activity-grid { grid-template-columns: 1fr; }
}
- .visitor-card:hover .visitor-avatar { border-color: var(--color-primary); }
- .visitor-avatar {
- width: 56px; height: 56px; border-radius: 50%;
- background: var(--color-secondary);
- border: 2px solid var(--color-secondary);
+ .activity-col {
+ background: var(--color-card);
+ border: 1px solid var(--color-secondary);
+ border-radius: 12px;
+ padding: 0.75rem 0.85rem 0.85rem;
+ }
+ .activity-col-header {
+ display: flex; align-items: center; justify-content: space-between;
+ margin-bottom: 0.7rem;
+ }
+ .activity-col-title {
+ font-size: 0.78rem; font-weight: 700; color: var(--color-muted);
+ text-transform: uppercase; letter-spacing: 0.05em;
+ }
+ .activity-col-link {
+ font-size: 0.75rem; color: var(--color-primary);
+ text-decoration: none; font-weight: 600;
+ }
+ .activity-col-link:hover { text-decoration: underline; }
+ .activity-row {
+ display: flex; gap: 0.5rem;
+ }
+ /* Avatar-Karte */
+ .soc-card {
+ flex: 1; display: flex; flex-direction: column; align-items: center; gap: 0.3rem;
+ text-decoration: none; color: var(--color-text); cursor: pointer; min-width: 0;
+ }
+ .soc-card:hover .soc-avatar { border-color: var(--color-primary); }
+ .soc-avatar {
+ width: 48px; height: 48px; border-radius: 50%;
+ background: var(--color-secondary); border: 2px solid var(--color-secondary);
display: flex; align-items: center; justify-content: center;
- font-size: 1.4rem; overflow: hidden; flex-shrink: 0;
- transition: border-color 0.15s;
+ font-size: 1.2rem; overflow: hidden; flex-shrink: 0;
+ transition: border-color 0.15s; position: relative;
}
- .visitor-avatar img { width: 100%; height: 100%; object-fit: cover; border-radius: 50%; }
- .visitor-name {
- font-size: 0.75rem; text-align: center;
- white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
- width: 100%;
+ .soc-avatar img { width: 100%; height: 100%; object-fit: cover; border-radius: 50%; }
+ .soc-lock {
+ position: absolute; inset: 0;
+ display: flex; align-items: center; justify-content: center; font-size: 0.95rem;
}
- .visitor-time { font-size: 0.68rem; color: var(--color-muted); text-align: center; }
-
- /* ── Dating: Likes & Matches ── */
- .dating-strip { display: flex; flex-wrap: wrap; gap: 0.75rem; }
- .dating-card {
- display: flex; flex-direction: column; align-items: center; gap: 0.3rem;
- text-decoration: none; color: var(--color-text); width: 72px;
- }
- .dating-card:hover .dating-avatar { border-color: var(--color-primary); }
- .dating-avatar {
- width: 56px; height: 56px; border-radius: 50%;
- background: var(--color-secondary);
- border: 2px solid var(--color-secondary);
- display: flex; align-items: center; justify-content: center;
- font-size: 1.4rem; overflow: hidden; flex-shrink: 0;
- transition: border-color 0.15s;
- }
- .dating-avatar img { width: 100%; height: 100%; object-fit: cover; border-radius: 50%; }
- .dating-name {
- font-size: 0.75rem; text-align: center;
+ .soc-name {
+ font-size: 0.68rem; text-align: center;
white-space: nowrap; overflow: hidden; text-overflow: ellipsis; width: 100%;
}
- /* Verschwommene Karte für nicht-Premium */
- .dating-card-locked {
- width: 72px; display: flex; flex-direction: column; align-items: center; gap: 0.3rem;
- cursor: default;
- }
- .dating-avatar-blurred {
- width: 56px; height: 56px; border-radius: 50%;
- background: var(--color-secondary);
- border: 2px solid var(--color-secondary);
- overflow: hidden; flex-shrink: 0; position: relative;
- }
- .dating-avatar-blurred img {
- width: 100%; height: 100%; object-fit: cover; border-radius: 50%;
- filter: blur(6px); transform: scale(1.1);
- }
- .dating-avatar-blurred .lock-icon {
- position: absolute; inset: 0;
- display: flex; align-items: center; justify-content: center;
- font-size: 1.1rem;
- }
- .premium-hint {
- font-size: 0.65rem; color: var(--color-primary); text-align: center; font-weight: 600;
- }
- .match-badge {
- font-size: 0.65rem; color: var(--color-primary); text-align: center; font-weight: 600;
- }
+ .soc-sub { font-size: 0.62rem; color: var(--color-muted); text-align: center; }
+ .soc-sub-accent { font-size: 0.62rem; color: var(--color-primary); font-weight: 600; text-align: center; }
+ .activity-empty { font-size: 0.8rem; color: var(--color-muted); text-align: center; padding: 0.5rem 0; }
/* ── Location-Events ── */
.loc-event-list { display: flex; flex-direction: column; gap: 0.6rem; }
@@ -130,6 +116,159 @@
.loc-event-title { font-size: 0.92rem; font-weight: 600;
white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.loc-event-date { font-size: 0.75rem; color: var(--color-primary); margin-top: 0.15rem; }
+
+ /* ── Aktive Spiele ── */
+ .active-game-list { display: flex; flex-direction: column; gap: 0.6rem; }
+ .active-game-card {
+ display: flex; gap: 0.75rem; align-items: center;
+ background: var(--color-secondary); border: 1px solid var(--color-secondary);
+ border-radius: 10px; padding: 0.65rem 0.85rem;
+ text-decoration: none; color: inherit;
+ transition: border-color 0.15s;
+ }
+ .active-game-card:hover { border-color: var(--color-primary); }
+ .active-game-icon {
+ width: 48px; height: 48px; border-radius: 8px; flex-shrink: 0;
+ background: var(--color-card);
+ display: flex; align-items: center; justify-content: center; font-size: 1.6rem;
+ }
+ .active-game-body { flex: 1; min-width: 0; }
+ .active-game-title { font-size: 0.92rem; font-weight: 600; }
+ .active-game-sub { font-size: 0.75rem; color: var(--color-muted); margin-top: 0.1rem; }
+ .active-game-action {
+ font-size: 0.8rem; color: var(--color-primary); font-weight: 600; flex-shrink: 0;
+ }
+
+ /* ── Einladungen ── */
+ .invite-list { display: flex; flex-direction: column; gap: 0.6rem; }
+ .invite-card {
+ display: flex; gap: 0.75rem; align-items: center;
+ background: var(--color-secondary); border: 1px solid var(--color-secondary);
+ border-radius: 10px; padding: 0.65rem 0.85rem;
+ text-decoration: none; color: inherit;
+ transition: border-color 0.15s;
+ }
+ .invite-card:hover { border-color: var(--color-primary); }
+ .invite-avatar {
+ width: 40px; height: 40px; border-radius: 50%; flex-shrink: 0;
+ background: var(--color-card);
+ display: flex; align-items: center; justify-content: center; font-size: 1.1rem;
+ overflow: hidden;
+ }
+ .invite-avatar img { width: 100%; height: 100%; object-fit: cover; border-radius: 50%; }
+ .invite-body { flex: 1; min-width: 0; }
+ .invite-from { font-size: 0.88rem; font-weight: 600;
+ white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
+ .invite-type { font-size: 0.73rem; color: var(--color-muted); margin-top: 0.1rem; }
+ .invite-action { font-size: 0.8rem; color: var(--color-primary); font-weight: 600; flex-shrink: 0; }
+
+ /* ── Freundschaftsanfragen ── */
+ .friend-req-strip { display: flex; flex-wrap: wrap; gap: 0.75rem; }
+ .friend-req-card {
+ display: flex; flex-direction: column; align-items: center; gap: 0.3rem;
+ text-decoration: none; color: var(--color-text); width: 72px;
+ }
+ .friend-req-card:hover .friend-req-avatar { border-color: var(--color-primary); }
+ .friend-req-avatar {
+ width: 56px; height: 56px; border-radius: 50%;
+ background: var(--color-secondary);
+ border: 2px solid var(--color-primary);
+ display: flex; align-items: center; justify-content: center;
+ font-size: 1.4rem; overflow: hidden; flex-shrink: 0;
+ transition: border-color 0.15s;
+ }
+ .friend-req-avatar img { width: 100%; height: 100%; object-fit: cover; border-radius: 50%; }
+ .friend-req-name {
+ font-size: 0.75rem; text-align: center;
+ white-space: nowrap; overflow: hidden; text-overflow: ellipsis; width: 100%;
+ }
+ .friend-req-badge { font-size: 0.65rem; color: var(--color-primary); font-weight: 600; text-align: center; }
+
+ /* ── Compose ── */
+ .post-compose { background:var(--color-card); border:1px solid var(--color-secondary); border-radius:10px; padding:1rem; margin-bottom:1rem; transition:border-color 0.15s; }
+ .post-compose.drag-over { border-color:var(--color-primary); background:rgba(var(--color-primary-rgb,180,0,60),0.06); }
+ .compose-type { display:flex; gap:1.5rem; margin-bottom:0.75rem; }
+ .compose-type label { display:flex; align-items:center; gap:0.4rem; font-size:0.9rem; cursor:pointer; }
+ .post-compose textarea { width:100%; padding:0.6rem 0.85rem; border:1px solid var(--color-secondary); border-radius:6px; background:var(--color-secondary); color:var(--color-text); font-size:0.95rem; outline:none; transition:border-color 0.2s; resize:vertical; min-height:70px; box-sizing:border-box; }
+ .post-compose textarea:focus { border-color:var(--color-primary); }
+ .compose-thumbs { display:none; flex-wrap:wrap; gap:0.5rem; margin-top:0.5rem; }
+ .compose-thumb { position:relative; width:64px; height:64px; flex-shrink:0; }
+ .compose-thumb img { width:64px; height:64px; object-fit:cover; border-radius:6px; display:block; }
+ .compose-thumb-remove { position:absolute; top:-5px; right:-5px; background:rgba(0,0,0,0.7); border:none; color:#fff; border-radius:50%; font-size:0.65rem; cursor:pointer; display:flex; align-items:center; justify-content:center; padding:0; margin:0; width:auto; line-height:1; }
+ .umfrage-options { margin-top:0.5rem; }
+ .umfrage-option-row { display:flex; gap:0.5rem; margin-bottom:0.4rem; }
+ .umfrage-option-row input { flex:1; }
+ .umfrage-option-row button { width:auto; margin:0; padding:0.3rem 0.6rem; font-size:0.8rem; }
+ .compose-footer { display:flex; justify-content:space-between; align-items:center; margin-top:0.75rem; flex-wrap:wrap; gap:0.5rem; }
+ .multi-toggle { font-size:0.85rem; display:flex; align-items:center; gap:0.4rem; }
+ .privacy-toggle { font-size:0.85rem; display:flex; align-items:center; gap:0.4rem; }
+ .compose-action-btn { background:none; border:1px solid var(--color-secondary); color:var(--color-muted); border-radius:6px; padding:0.35rem 0.6rem; font-size:0.95rem; cursor:pointer; margin:0; width:auto; transition:border-color 0.15s,color 0.15s; }
+ .compose-action-btn:hover { border-color:var(--color-primary); color:var(--color-primary); background:none; }
+ label.compose-action-btn { display:inline-flex; align-items:center; }
+
+ /* ── Post Cards (1:1 wie Feed) ── */
+ .post-card { background:var(--color-card); border:1px solid var(--color-secondary); border-radius:10px; padding:1rem; margin-bottom:0.9rem; cursor:pointer; transition:border-color 0.15s; }
+ .post-card:hover { border-color:var(--color-primary); }
+ .post-header { display:flex; align-items:center; gap:0.7rem; margin-bottom:0.6rem; }
+ .post-avatar { width:36px; height:36px; border-radius:50%; background:var(--color-secondary); display:flex; align-items:center; justify-content:center; font-size:0.95rem; flex-shrink:0; overflow:hidden; }
+ .post-avatar img { width:100%; height:100%; object-fit:cover; }
+ .post-author { font-weight:600; font-size:0.9rem; }
+ .post-meta { font-size:0.75rem; color:var(--color-muted); }
+ .post-text { font-size:0.95rem; line-height:1.5; white-space:pre-wrap; word-break:break-word; }
+ .post-bild { width:100%; max-height:400px; object-fit:contain; border-radius:6px; margin-top:0.5rem; display:block; }
+ .post-actions { display:flex; gap:1rem; margin-top:0.75rem; align-items:center; flex-wrap:wrap; }
+ .post-action-btn { background:none; border:none; color:var(--color-muted); font-size:0.85rem; padding:0; display:flex; align-items:center; gap:0.3rem; margin:0; width:auto; pointer-events:none; }
+ .post-action-btn.active { color:var(--color-primary); }
+ .gruppe-badge { display:inline-flex; align-items:center; gap:0.3rem; font-size:0.75rem; color:var(--color-muted); background:var(--color-secondary); border-radius:4px; padding:0.15rem 0.45rem; margin-left:0.3rem; }
+ .umfrage-option-bar { margin:0.3rem 0; border-radius:6px; overflow:hidden; border:1px solid var(--color-secondary); position:relative; }
+ .umfrage-option-bar.voted { border-color:var(--color-primary); }
+ .umfrage-bar-fill { position:absolute; inset:0; background:rgba(var(--color-primary-rgb,180,0,60),0.15); }
+ .umfrage-bar-content { position:relative; display:flex; justify-content:space-between; padding:0.45rem 0.75rem; font-size:0.88rem; }
+ .umfrage-total { font-size:0.78rem; color:var(--color-muted); margin-top:0.3rem; }
+
+ /* ── Neue Mitglieder ── */
+ .new-members-strip {
+ display: flex;
+ gap: 0.75rem;
+ overflow-x: auto;
+ padding-bottom: 0.4rem;
+ scrollbar-width: thin;
+ scrollbar-color: var(--color-secondary) transparent;
+ }
+ .new-members-strip::-webkit-scrollbar { height: 4px; }
+ .new-members-strip::-webkit-scrollbar-thumb { background: var(--color-secondary); border-radius: 2px; }
+ .nm-card {
+ flex: 0 0 160px;
+ background: var(--color-card);
+ border: 1px solid var(--color-secondary);
+ border-radius: 12px;
+ overflow: hidden;
+ text-decoration: none;
+ color: var(--color-text);
+ display: flex;
+ flex-direction: column;
+ transition: border-color 0.15s, box-shadow 0.15s;
+ }
+ .nm-card:hover { border-color: var(--color-primary); box-shadow: 0 4px 18px rgba(0,0,0,0.35); }
+ .nm-card-img {
+ width: 100%; aspect-ratio: 1; flex-shrink: 0;
+ overflow: hidden; background: var(--color-secondary);
+ display: flex; align-items: center; justify-content: center;
+ font-size: 2.5rem; position: relative;
+ }
+ .nm-card-img img { width: 100%; height: 100%; object-fit: cover; display: block; }
+ .nm-card-body { padding: 0.6rem 0.65rem; display: flex; flex-direction: column; gap: 0.25rem; }
+ .nm-card-name { font-weight: 700; font-size: 0.9rem; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
+ .nm-card-meta { display: flex; flex-wrap: wrap; gap: 0.25rem; }
+ .nm-card-chip {
+ padding: 0.1rem 0.4rem; border-radius: 20px;
+ background: var(--color-secondary); font-size: 0.7rem; color: var(--color-muted);
+ }
+ .nm-card-desc {
+ font-size: 0.75rem; color: var(--color-muted); line-height: 1.35;
+ overflow: hidden; display: -webkit-box;
+ -webkit-line-clamp: 2; -webkit-box-orient: vertical;
+ }
@@ -138,22 +277,62 @@
Home
-
-
+