593 lines
19 KiB
Vue
Executable File
593 lines
19 KiB
Vue
Executable File
<template>
|
|
<div class="account-settings">
|
|
<section class="account-settings__hero surface-card">
|
|
<span class="account-settings__eyebrow">{{ $t("settings.security.heroEyebrow") }}</span>
|
|
<h2>{{ $t("settings.security.title") }}</h2>
|
|
<p>{{ $t("settings.security.heroIntro") }}</p>
|
|
</section>
|
|
|
|
<section class="account-settings__panel surface-card">
|
|
<div class="account-settings__grid">
|
|
<label class="account-settings__field">
|
|
<span>{{ $t("settings.security.username") }}</span>
|
|
<input type="text" v-model="username" :placeholder="$t('settings.security.username')" />
|
|
</label>
|
|
|
|
<label class="account-settings__field">
|
|
<span>{{ $t("settings.security.email") }}</span>
|
|
<input type="text" v-model="email" :placeholder="$t('settings.security.email')" />
|
|
</label>
|
|
|
|
<label class="account-settings__field">
|
|
<span>{{ $t("settings.security.newpassword") }}</span>
|
|
<input type="password" v-model="newpassword" :placeholder="$t('settings.security.newpassword')"
|
|
autocomplete="new-password" :class="{ 'field-error': newpassword && !isNewPasswordValid }" />
|
|
<span v-if="newpassword && !isNewPasswordValid" class="form-error">{{ $t("settings.security.validation.newPasswordTooShort") }}</span>
|
|
</label>
|
|
|
|
<label class="account-settings__field">
|
|
<span>{{ $t("settings.security.newpasswordretype") }}</span>
|
|
<input type="password" v-model="newpasswordretype"
|
|
:placeholder="$t('settings.security.newpasswordretype')" autocomplete="new-password"
|
|
:class="{ 'field-error': newpasswordretype && !passwordsMatch }" />
|
|
<span v-if="newpasswordretype && !passwordsMatch" class="form-error">{{ $t("settings.security.validation.passwordMismatch") }}</span>
|
|
</label>
|
|
|
|
<label class="account-settings__field account-settings__field--full">
|
|
<span>{{ $t("settings.security.oldpassword") }}</span>
|
|
<input type="password" v-model="oldpassword" :placeholder="$t('settings.security.oldpassword')"
|
|
autocomplete="current-password" :class="{ 'field-error': requiresOldPassword && !oldpassword.trim() }" />
|
|
<span v-if="requiresOldPassword && !oldpassword.trim()" class="form-error">{{ $t("settings.security.validation.oldPasswordRequired") }}</span>
|
|
</label>
|
|
</div>
|
|
|
|
<label class="account-settings__toggle">
|
|
<input type="checkbox" v-model="showInSearch" />
|
|
<span>{{ $t("settings.security.showinsearch") }}</span>
|
|
</label>
|
|
|
|
<section class="account-settings__oauth surface-card">
|
|
<h3>{{ $t("settings.security.oauthTitle") }}</h3>
|
|
<p>{{ $t("settings.security.oauthIntro") }}</p>
|
|
|
|
<div v-if="oauthIdentities.length > 0" class="account-settings__oauth-list">
|
|
<div v-for="identity in oauthIdentities" :key="identity.id" class="oauth-identity-item">
|
|
<div class="oauth-identity-item__info">
|
|
<strong>{{ identity.displayName }}</strong>
|
|
<span class="oauth-identity-item__date">{{ formatDate(identity.createdAt) }}</span>
|
|
</div>
|
|
<button
|
|
type="button"
|
|
class="oauth-identity-item__remove"
|
|
@click="removeOAuthIdentity(identity.id)"
|
|
:disabled="oauthLoading"
|
|
title="Authentifizierung entfernen"
|
|
>
|
|
✕
|
|
</button>
|
|
</div>
|
|
</div>
|
|
<p v-else class="oauth-identity-item__empty">{{ $t("settings.security.oauthNoIdentities") }}</p>
|
|
|
|
<div class="account-settings__oauth-actions">
|
|
<div v-if="availableProviders.length > 0" class="oauth-provider-buttons">
|
|
<button
|
|
v-for="provider in availableProviders"
|
|
:key="provider.slug"
|
|
type="button"
|
|
class="oauth-provider-button"
|
|
:class="`oauth-provider-button--${provider.slug}`"
|
|
:disabled="oauthLoading"
|
|
@click="startAddOAuthIdentity(provider.slug)"
|
|
>
|
|
{{ provider.label }} hinzufügen
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</section>
|
|
|
|
<section class="account-settings__adult surface-card">
|
|
<h3>{{ $t("settings.security.adultAccessTitle") }}</h3>
|
|
<p>{{ $t("settings.security.adultAccessIntro") }}</p>
|
|
<div class="account-settings__adult-status">
|
|
<strong>{{ adultStatusTitle }}</strong>
|
|
<span>{{ adultStatusText }}</span>
|
|
<span v-if="adultVerificationRequest?.originalName">{{ adultVerificationRequest.originalName }}</span>
|
|
</div>
|
|
<div class="account-settings__adult-actions" v-if="canRequestAdultVerification">
|
|
<router-link to="/socialnetwork/erotic/access">{{ $t("settings.security.requestAdultVerification") }}</router-link>
|
|
</div>
|
|
</section>
|
|
|
|
<div class="account-settings__actions">
|
|
<button @click="changeAccount" :disabled="oauthLoading">{{ $t("settings.security.changeaction") }}</button>
|
|
</div>
|
|
</section>
|
|
</div>
|
|
</template>
|
|
|
|
<script>
|
|
import apiClient from '@/utils/axios.js';
|
|
import { mapGetters } from 'vuex';
|
|
import { showApiError, showError, showInfo, showSuccess } from '@/utils/feedback.js';
|
|
|
|
export default {
|
|
name: "AccountSettingsView",
|
|
components: {},
|
|
data() {
|
|
return {
|
|
username: "",
|
|
email: "",
|
|
newpassword: "",
|
|
newpasswordretype: "",
|
|
showInSearch: false,
|
|
oldpassword: "",
|
|
isAdult: false,
|
|
adultVerificationStatus: "none",
|
|
adultVerificationRequest: null,
|
|
oauthIdentities: [],
|
|
oauthProviders: [],
|
|
oauthLoading: false,
|
|
};
|
|
},
|
|
computed: {
|
|
...mapGetters(['user', 'socket']),
|
|
requiresOldPassword() {
|
|
return this.newpassword.trim().length > 0;
|
|
},
|
|
isNewPasswordValid() {
|
|
return this.newpassword.length === 0 || this.newpassword.length >= 8;
|
|
},
|
|
passwordsMatch() {
|
|
return this.newpassword === this.newpasswordretype;
|
|
},
|
|
canRequestAdultVerification() {
|
|
return this.isAdult && ['none', 'rejected'].includes(this.adultVerificationStatus);
|
|
},
|
|
adultStatusTitle() {
|
|
if (!this.isAdult) {
|
|
return this.$t('settings.security.adultStatus.ineligible.title');
|
|
}
|
|
return this.$t(`settings.security.adultStatus.${this.adultVerificationStatus}.title`);
|
|
},
|
|
adultStatusText() {
|
|
if (!this.isAdult) {
|
|
return this.$t('settings.security.adultStatus.ineligible.body');
|
|
}
|
|
return this.$t(`settings.security.adultStatus.${this.adultVerificationStatus}.body`);
|
|
},
|
|
availableProviders() {
|
|
return this.oauthProviders.filter(p => p.configured && !this.oauthIdentities.some(id => id.provider === p.slug));
|
|
}
|
|
},
|
|
methods: {
|
|
formatDate(date) {
|
|
return new Date(date).toLocaleDateString();
|
|
},
|
|
async loadAccount() {
|
|
const response = await apiClient.post('/api/settings/account', { userId: this.user.id });
|
|
this.username = response.data.username;
|
|
this.showInSearch = response.data.showinsearch;
|
|
this.email = response.data.email;
|
|
this.isAdult = !!response.data.isAdult;
|
|
this.adultVerificationStatus = response.data.adultVerificationStatus || 'none';
|
|
this.adultVerificationRequest = response.data.adultVerificationRequest || null;
|
|
},
|
|
async loadOAuthIdentities() {
|
|
try {
|
|
const response = await apiClient.get('/api/auth/oauth/user/identities');
|
|
this.oauthIdentities = response.data.identities || [];
|
|
} catch (error) {
|
|
console.error('Error loading OAuth identities:', error);
|
|
this.oauthIdentities = [];
|
|
}
|
|
},
|
|
async loadOAuthProviders() {
|
|
try {
|
|
const response = await apiClient.get('/api/auth/oauth/providers');
|
|
this.oauthProviders = response.data.providers || [];
|
|
} catch (error) {
|
|
console.error('Error loading OAuth providers:', error);
|
|
this.oauthProviders = [];
|
|
}
|
|
},
|
|
startAddOAuthIdentity(providerSlug) {
|
|
if (this.oauthLoading) {
|
|
return;
|
|
}
|
|
this.oauthLoading = true;
|
|
const uid = this.user?.id || '';
|
|
window.location.href = `/api/auth/oauth/user/${encodeURIComponent(providerSlug)}/start?userid=${encodeURIComponent(uid)}`;
|
|
},
|
|
async removeOAuthIdentity(identityId) {
|
|
if (!confirm(this.$t('settings.security.oauthRemoveConfirm'))) {
|
|
return;
|
|
}
|
|
|
|
try {
|
|
this.oauthLoading = true;
|
|
await apiClient.delete(`/api/auth/oauth/user/${identityId}`);
|
|
showSuccess(this, 'Authentifizierung entfernt');
|
|
await this.loadOAuthIdentities();
|
|
} catch (error) {
|
|
console.error('Error removing OAuth identity:', error);
|
|
showApiError(this, error, 'Fehler beim Entfernen der Authentifizierung');
|
|
} finally {
|
|
this.oauthLoading = false;
|
|
}
|
|
},
|
|
registerSocketListeners(sock) {
|
|
if (!sock) return;
|
|
this._adultVerificationChangedHandler = async (payload = {}) => {
|
|
await this.loadAccount();
|
|
if (payload.status === 'approved') {
|
|
showInfo(this, this.$t('socialnetwork.erotic.notifications.approved'));
|
|
} else if (payload.status === 'rejected') {
|
|
showInfo(this, this.$t('socialnetwork.erotic.notifications.rejected'));
|
|
}
|
|
};
|
|
sock.on('adultVerificationChanged', this._adultVerificationChangedHandler);
|
|
},
|
|
unregisterSocketListeners() {
|
|
if (this.socket && this._adultVerificationChangedHandler) {
|
|
this.socket.off('adultVerificationChanged', this._adultVerificationChangedHandler);
|
|
}
|
|
this._adultVerificationChangedHandler = null;
|
|
},
|
|
async changeAccount() {
|
|
try {
|
|
// Prüfe ob ein neues Passwort eingegeben wurde
|
|
const hasNewPassword = this.newpassword && this.newpassword.trim() !== '';
|
|
|
|
if (hasNewPassword) {
|
|
if (!this.isNewPasswordValid) {
|
|
showError(this, 'tr:settings.security.validation.newPasswordTooShort');
|
|
return;
|
|
}
|
|
// Validiere Passwort-Wiederholung nur wenn ein neues Passwort eingegeben wurde
|
|
if (!this.passwordsMatch) {
|
|
showError(this, 'tr:settings.security.validation.passwordMismatch');
|
|
return;
|
|
}
|
|
|
|
// Prüfe ob das alte Passwort eingegeben wurde
|
|
if (!this.oldpassword || this.oldpassword.trim() === '') {
|
|
showError(this, 'tr:settings.security.validation.oldPasswordRequired');
|
|
return;
|
|
}
|
|
}
|
|
|
|
// Bereite die Daten für den API-Aufruf vor
|
|
const accountData = {
|
|
userId: this.user.id,
|
|
settings: {
|
|
username: this.username,
|
|
email: this.email,
|
|
showinsearch: this.showInSearch
|
|
}
|
|
};
|
|
|
|
// Füge Passwort-Daten nur hinzu, wenn ein neues Passwort eingegeben wurde
|
|
if (hasNewPassword) {
|
|
accountData.settings.newpassword = this.newpassword;
|
|
accountData.settings.oldpassword = this.oldpassword;
|
|
}
|
|
|
|
// API-Aufruf zum Speichern der Account-Einstellungen
|
|
await apiClient.post('/api/settings/set-account', accountData);
|
|
|
|
showSuccess(this, 'tr:settings.security.feedback.saved');
|
|
|
|
// Leere die Passwort-Felder nach erfolgreichem Speichern
|
|
this.newpassword = '';
|
|
this.newpasswordretype = '';
|
|
this.oldpassword = '';
|
|
|
|
} catch (error) {
|
|
console.error('Fehler beim Speichern der Account-Einstellungen:', error);
|
|
showApiError(this, error, 'tr:settings.security.feedback.saveError');
|
|
}
|
|
},
|
|
|
|
},
|
|
async mounted() {
|
|
if (this.socket) {
|
|
this.registerSocketListeners(this.socket);
|
|
}
|
|
await this.loadAccount();
|
|
await this.loadOAuthProviders();
|
|
await this.loadOAuthIdentities();
|
|
|
|
// Stelle sicher, dass Passwort-Felder leer sind
|
|
this.newpassword = '';
|
|
this.newpasswordretype = '';
|
|
this.oldpassword = '';
|
|
},
|
|
watch: {
|
|
socket(newSocket) {
|
|
this.unregisterSocketListeners();
|
|
if (newSocket) {
|
|
this.registerSocketListeners(newSocket);
|
|
}
|
|
}
|
|
},
|
|
beforeUnmount() {
|
|
this.unregisterSocketListeners();
|
|
}
|
|
};
|
|
</script>
|
|
|
|
<style lang="scss" scoped>
|
|
.account-settings {
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 18px;
|
|
max-width: 960px;
|
|
width: 100%;
|
|
min-height: 0;
|
|
flex: 1 1 0%;
|
|
align-self: stretch;
|
|
}
|
|
|
|
.account-settings__hero,
|
|
.account-settings__panel,
|
|
.account-settings__adult,
|
|
.account-settings__oauth {
|
|
background: linear-gradient(180deg, rgba(255, 252, 247, 0.97), rgba(250, 244, 235, 0.95));
|
|
border: 1px solid var(--color-border);
|
|
border-radius: var(--radius-lg);
|
|
box-shadow: var(--shadow-soft);
|
|
}
|
|
|
|
.account-settings__hero {
|
|
flex: 0 0 auto;
|
|
padding: 26px 28px;
|
|
}
|
|
|
|
.account-settings__panel {
|
|
flex: 0 0 auto;
|
|
padding: 28px;
|
|
}
|
|
|
|
.account-settings__oauth {
|
|
flex: 0 0 auto;
|
|
padding: 20px 24px;
|
|
display: grid;
|
|
gap: 16px;
|
|
}
|
|
|
|
.account-settings__oauth h3 {
|
|
margin: 0;
|
|
}
|
|
|
|
.account-settings__oauth p {
|
|
margin: 0;
|
|
color: var(--color-text-secondary);
|
|
font-size: 0.95rem;
|
|
}
|
|
|
|
.account-settings__oauth-list {
|
|
display: grid;
|
|
gap: 8px;
|
|
}
|
|
|
|
.oauth-identity-item {
|
|
display: flex;
|
|
justify-content: space-between;
|
|
align-items: center;
|
|
padding: 12px 14px;
|
|
background: rgba(255, 255, 255, 0.4);
|
|
border: 1px solid rgba(200, 150, 100, 0.15);
|
|
border-radius: 10px;
|
|
}
|
|
|
|
.oauth-identity-item__info {
|
|
display: grid;
|
|
gap: 4px;
|
|
}
|
|
|
|
.oauth-identity-item__info strong {
|
|
font-size: 0.95rem;
|
|
}
|
|
|
|
.oauth-identity-item__date {
|
|
font-size: 0.85rem;
|
|
color: var(--color-text-secondary);
|
|
}
|
|
|
|
.oauth-identity-item__remove {
|
|
background: rgba(200, 80, 80, 0.15);
|
|
border: 1px solid rgba(200, 80, 80, 0.3);
|
|
color: #a94442;
|
|
width: 28px;
|
|
height: 28px;
|
|
border-radius: 6px;
|
|
cursor: pointer;
|
|
font-weight: 600;
|
|
transition: all 0.2s ease;
|
|
}
|
|
|
|
.oauth-identity-item__remove:hover:not(:disabled) {
|
|
background: rgba(200, 80, 80, 0.25);
|
|
}
|
|
|
|
.oauth-identity-item__remove:disabled {
|
|
opacity: 0.5;
|
|
cursor: not-allowed;
|
|
}
|
|
|
|
.oauth-identity-item__empty {
|
|
margin: 0;
|
|
color: var(--color-text-secondary);
|
|
font-size: 0.95rem;
|
|
font-style: italic;
|
|
}
|
|
|
|
.account-settings__oauth-actions {
|
|
padding-top: 8px;
|
|
border-top: 1px solid rgba(200, 150, 100, 0.1);
|
|
}
|
|
|
|
.oauth-provider-buttons {
|
|
display: grid;
|
|
grid-template-columns: repeat(auto-fit, minmax(160px, 1fr));
|
|
gap: 10px;
|
|
}
|
|
|
|
.oauth-provider-button {
|
|
padding: 10px 14px;
|
|
border-radius: 10px;
|
|
border: 1px solid rgba(150, 100, 80, 0.2);
|
|
background: linear-gradient(180deg, rgba(255, 255, 255, 0.6), rgba(248, 240, 230, 0.6));
|
|
color: #5a4a42;
|
|
font-weight: 600;
|
|
font-size: 0.9rem;
|
|
cursor: pointer;
|
|
transition: all 0.2s ease;
|
|
}
|
|
|
|
.oauth-provider-button:hover:not(:disabled) {
|
|
transform: translateY(-1px);
|
|
background: linear-gradient(180deg, rgba(255, 255, 255, 0.8), rgba(250, 245, 235, 0.8));
|
|
}
|
|
|
|
.oauth-provider-button:disabled {
|
|
opacity: 0.6;
|
|
cursor: not-allowed;
|
|
}
|
|
|
|
.oauth-provider-button--google {
|
|
border-left: 4px solid #4285f4;
|
|
}
|
|
|
|
.oauth-provider-button--microsoft {
|
|
border-left: 4px solid #0078d4;
|
|
}
|
|
|
|
.oauth-provider-button--keycloak {
|
|
border-left: 4px solid #d67f2f;
|
|
}
|
|
|
|
.oauth-provider-button--ory {
|
|
border-left: 4px solid #0f766e;
|
|
}
|
|
|
|
.oauth-provider-button--zitadel {
|
|
border-left: 4px solid #0f5132;
|
|
}
|
|
|
|
.account-settings__adult {
|
|
flex: 0 0 auto;
|
|
padding: 20px 24px;
|
|
}
|
|
|
|
.account-settings__actions {
|
|
display: flex;
|
|
gap: 12px;
|
|
padding-top: 8px;
|
|
}
|
|
|
|
.account-settings__actions button {
|
|
padding: 10px 20px;
|
|
border-radius: 8px;
|
|
border: 1px solid rgba(150, 100, 80, 0.3);
|
|
background: #4a8f3d;
|
|
color: white;
|
|
font-weight: 600;
|
|
cursor: pointer;
|
|
transition: all 0.2s ease;
|
|
}
|
|
|
|
.account-settings__actions button:hover:not(:disabled) {
|
|
background: #3d7631;
|
|
}
|
|
|
|
.account-settings__actions button:disabled {
|
|
opacity: 0.6;
|
|
cursor: not-allowed;
|
|
}
|
|
|
|
.account-settings__grid {
|
|
display: grid;
|
|
grid-template-columns: repeat(2, minmax(0, 1fr));
|
|
gap: 18px;
|
|
margin-bottom: 18px;
|
|
}
|
|
|
|
.account-settings__field {
|
|
display: grid;
|
|
gap: 6px;
|
|
font-size: 0.95rem;
|
|
}
|
|
|
|
.account-settings__field span {
|
|
font-weight: 600;
|
|
}
|
|
|
|
.account-settings__field input {
|
|
padding: 10px;
|
|
border: 1px solid rgba(150, 100, 80, 0.2);
|
|
border-radius: 8px;
|
|
font-size: 0.95rem;
|
|
}
|
|
|
|
.account-settings__field--full {
|
|
grid-column: 1 / -1;
|
|
}
|
|
|
|
.form-error {
|
|
color: #a94442;
|
|
font-size: 0.85rem;
|
|
}
|
|
|
|
.account-settings__toggle {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 10px;
|
|
cursor: pointer;
|
|
}
|
|
|
|
.account-settings__toggle input[type="checkbox"] {
|
|
cursor: pointer;
|
|
}
|
|
|
|
.account-settings__adult-status {
|
|
display: grid;
|
|
gap: 6px;
|
|
padding: 12px;
|
|
background: rgba(248, 240, 230, 0.6);
|
|
border-radius: 8px;
|
|
margin: 12px 0;
|
|
}
|
|
|
|
.account-settings__adult-actions {
|
|
padding-top: 12px;
|
|
}
|
|
|
|
.account-settings__adult-actions a {
|
|
display: inline-block;
|
|
padding: 10px 16px;
|
|
background: #4a8f3d;
|
|
color: white;
|
|
border-radius: 8px;
|
|
text-decoration: none;
|
|
font-weight: 600;
|
|
transition: all 0.2s ease;
|
|
}
|
|
|
|
.account-settings__adult-actions a:hover {
|
|
background: #3d7631;
|
|
}
|
|
|
|
@media (max-width: 768px) {
|
|
.account-settings__grid,
|
|
.oauth-provider-buttons {
|
|
grid-template-columns: 1fr;
|
|
}
|
|
|
|
.account-settings__field--full {
|
|
grid-column: 1;
|
|
}
|
|
}
|
|
</style>
|