Add account deletion feature and privacy section
Implement a new endpoint for account deletion in AuthController, allowing users to permanently delete their accounts and associated data. Update AuthService to handle account deletion logic, including confirmation checks and data removal from the database. Enhance frontend with new views and components for account deletion and privacy information, including links in the side menu and profile view. Update mobile app to support account deletion and privacy sections, improving user experience and compliance with data protection standards.
This commit is contained in:
72
PLAYSTORE_DATENSCHUTZERKLAERUNG.md
Normal file
72
PLAYSTORE_DATENSCHUTZERKLAERUNG.md
Normal file
@@ -0,0 +1,72 @@
|
||||
# Datenschutz und Account-Löschung für Google Play
|
||||
|
||||
**App:** TimeClock / Stechuhr
|
||||
**Datenschutz-URL:** https://stechuhr3.tsschulz.de/datenschutz
|
||||
**Account-Lösch-URL:** https://stechuhr3.tsschulz.de/account-loeschen
|
||||
**Stand:** 15.05.2026
|
||||
|
||||
## Vor Veröffentlichung ergänzen
|
||||
|
||||
- Ladungsfähige Anschrift des Verantwortlichen in der Web-Datenschutzerklärung ersetzen:
|
||||
`BITTE VOR VERÖFFENTLICHUNG ERGÄNZEN`
|
||||
- Prüfen, ob ein Datenschutzbeauftragter benannt werden muss.
|
||||
- Konkrete Backup- und Server-Log-Löschfristen ergänzen, falls diese festgelegt sind.
|
||||
|
||||
## Kurztext für Play Console / Datenschutzangaben
|
||||
|
||||
TimeClock ist eine Zeiterfassungsanwendung. Die App verarbeitet personenbezogene Daten zur Anmeldung, Zeiterfassung, Korrektur, Auswertung und Verwaltung persönlicher Arbeitszeiten.
|
||||
|
||||
Verantwortlich für die Datenverarbeitung ist Torsten Schulz. Kontakt: kontakt@tsschulz.de.
|
||||
|
||||
Die Datenschutzerklärung ist öffentlich abrufbar unter:
|
||||
https://stechuhr3.tsschulz.de/datenschutz
|
||||
|
||||
Die öffentliche Seite zur Account- und Datenlöschung ist abrufbar unter:
|
||||
https://stechuhr3.tsschulz.de/account-loeschen
|
||||
|
||||
## FAQ-Text für den Play Store
|
||||
|
||||
### Welche Daten verarbeitet TimeClock?
|
||||
|
||||
TimeClock verarbeitet Accountdaten wie Name, E-Mail-Adresse, Passwort-Hash und Login-Status. Außerdem verarbeitet die App Zeiterfassungsdaten wie Kommen, Gehen, Pausen, Korrekturen und Zeitstempel sowie Abwesenheitsdaten wie Urlaub, Krankheit, Feiertage und Arbeitszeitwünsche. Zusätzlich werden Einstellungen wie Bundesland, Soll-Arbeitszeit, Anzeigeoptionen und Berechtigungen gespeichert. Für Betrieb und Sicherheit können technische Daten wie IP-Adresse, Zugriffszeitpunkt, Geräte-/Browserinformationen und Server-Logs verarbeitet werden.
|
||||
|
||||
Bei optionaler Google-Anmeldung verarbeitet TimeClock die von Google übermittelte Google-Konto-ID, E-Mail-Adresse und den Anzeigenamen. Das Google-Passwort wird nicht an TimeClock übermittelt oder gespeichert.
|
||||
|
||||
### Wofür werden die Daten genutzt?
|
||||
|
||||
Die Daten werden genutzt, um die Zeiterfassung bereitzustellen, Nutzer zu authentifizieren, Arbeitszeiten zu speichern und auszuwerten, Korrekturen und Abwesenheiten zu verwalten, den Dienst abzusichern, Fehler zu analysieren und gesetzliche oder vertragliche Anforderungen an Arbeitszeitdaten zu erfüllen.
|
||||
|
||||
### Auf welcher Rechtsgrundlage werden Daten verarbeitet?
|
||||
|
||||
Die Verarbeitung erfolgt insbesondere auf Grundlage von Art. 6 Abs. 1 lit. b DSGVO zur Bereitstellung des Accounts und der Zeiterfassung, Art. 6 Abs. 1 lit. c DSGVO für gesetzliche Aufbewahrungs- oder Nachweispflichten, Art. 6 Abs. 1 lit. f DSGVO für Sicherheit, Fehleranalyse, Missbrauchsschutz und Server-Logs sowie Art. 6 Abs. 1 lit. a DSGVO für optionale Funktionen, soweit eine Einwilligung erforderlich ist.
|
||||
|
||||
### Werden Daten verkauft oder zu Werbezwecken weitergegeben?
|
||||
|
||||
Nein. Personenbezogene Daten werden nicht verkauft. Eine Weitergabe erfolgt nur, soweit sie für Betrieb, Authentifizierung, Hosting, E-Mail-Versand, Fehleranalyse oder gesetzliche Pflichten erforderlich ist.
|
||||
|
||||
### Welche Drittanbieter können beteiligt sein?
|
||||
|
||||
Für den Betrieb können Hosting-/Serveranbieter und E-Mail-Dienstleister eingesetzt werden. Bei Nutzung der optionalen Google-Anmeldung ist Google als OAuth-Anbieter beteiligt.
|
||||
|
||||
### Wie lange werden Daten gespeichert?
|
||||
|
||||
Account-, Zeit- und Einstellungsdaten werden grundsätzlich gespeichert, solange der Account besteht. Nach Account-Löschung werden die aktiven Accountdaten, Anmeldedaten, Google-Verknüpfungen, Zeitbuchungen, Korrekturen, Urlaubs- und Krankmeldungen sowie persönliche Einstellungen gelöscht. Server-Logs und Backups können technisch bedingt bis zum Ablauf regulärer Löschzyklen weiterbestehen. Daten können länger aufbewahrt werden, wenn gesetzliche Pflichten oder die Geltendmachung, Ausübung oder Verteidigung von Rechtsansprüchen dies erfordern.
|
||||
|
||||
### Wie können Nutzer ihren Account löschen?
|
||||
|
||||
Eingeloggte Nutzer können ihren Account in der Web-UI unter "Persönliches" löschen. Zusätzlich steht eine öffentliche Löschseite zur Verfügung:
|
||||
https://stechuhr3.tsschulz.de/account-loeschen
|
||||
|
||||
Über diese Seite können Nutzer auch ohne installierte App eine Löschung per E-Mail an kontakt@tsschulz.de beantragen.
|
||||
|
||||
### Welche Daten werden bei Account-Löschung gelöscht?
|
||||
|
||||
Gelöscht werden Accountdaten, Anmeldedaten, Google-Verknüpfungen, Zeitbuchungen, Zeitkorrekturen, Urlaubs- und Krankmeldungen, Zeitwünsche, Wochenarbeitszeiten und persönliche Einstellungen.
|
||||
|
||||
### Welche Rechte haben Nutzer?
|
||||
|
||||
Nutzer können Auskunft, Berichtigung, Löschung, Einschränkung der Verarbeitung und Datenübertragbarkeit verlangen sowie einer Verarbeitung widersprechen. Außerdem besteht ein Beschwerderecht bei einer zuständigen Datenschutzaufsichtsbehörde. Anfragen können an kontakt@tsschulz.de gerichtet werden.
|
||||
|
||||
### Wie werden Daten geschützt?
|
||||
|
||||
Die Übertragung erfolgt über HTTPS. Passwörter werden nicht im Klartext gespeichert. Zugriffstokens und OAuth-Verknüpfungen werden serverseitig verwaltet. Zugriffe werden auf das erforderliche Maß beschränkt.
|
||||
@@ -257,7 +257,31 @@ class AuthController {
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Eigenen Account und personenbezogene Daten löschen
|
||||
* DELETE /api/auth/account
|
||||
*/
|
||||
async deleteAccount(req, res) {
|
||||
try {
|
||||
const userId = req.user.userId;
|
||||
const { confirmation } = req.body;
|
||||
|
||||
await authService.deleteAccount(userId, confirmation);
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
message: 'Account wurde gelöscht'
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Fehler beim Löschen des Accounts:', error);
|
||||
|
||||
res.status(400).json({
|
||||
success: false,
|
||||
error: error.message
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = new AuthController();
|
||||
|
||||
|
||||
@@ -20,6 +20,7 @@ router.post('/logout', authenticateToken, authController.logout.bind(authControl
|
||||
router.get('/me', authenticateToken, authController.getCurrentUser.bind(authController));
|
||||
router.post('/change-password', authenticateToken, authController.changePassword.bind(authController));
|
||||
router.get('/validate', authenticateToken, authController.validateToken.bind(authController));
|
||||
router.delete('/account', authenticateToken, authController.deleteAccount.bind(authController));
|
||||
router.post('/google/link-url', authenticateToken, oauthController.createGoogleLinkUrl.bind(oauthController));
|
||||
router.post('/oauth/link-current', authenticateToken, oauthController.linkPendingToCurrentUser.bind(oauthController));
|
||||
router.get('/identities', authenticateToken, oauthController.getIdentities.bind(oauthController));
|
||||
|
||||
@@ -2,6 +2,7 @@ const bcrypt = require('bcrypt');
|
||||
const jwt = require('jsonwebtoken');
|
||||
const crypto = require('crypto');
|
||||
const nodemailer = require('nodemailer');
|
||||
const { Op } = require('sequelize');
|
||||
const database = require('../config/database');
|
||||
|
||||
/**
|
||||
@@ -667,6 +668,86 @@ ${frontendUrl}
|
||||
return true;
|
||||
}
|
||||
|
||||
async destroyOptional(model, where, transaction) {
|
||||
try {
|
||||
return await model.destroy({ where, transaction });
|
||||
} catch (error) {
|
||||
if (error.original?.code === 'ER_NO_SUCH_TABLE') {
|
||||
return 0;
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Eigenen Account inkl. personenbezogener Daten löschen.
|
||||
* @param {number} userId - Benutzer-ID
|
||||
* @param {string} confirmation - Muss "ACCOUNT LÖSCHEN" sein
|
||||
* @returns {Promise<boolean>} Erfolg
|
||||
*/
|
||||
async deleteAccount(userId, confirmation) {
|
||||
if (confirmation !== 'ACCOUNT LÖSCHEN') {
|
||||
throw new Error('Bitte bestätigen Sie die Löschung mit "ACCOUNT LÖSCHEN"');
|
||||
}
|
||||
|
||||
const {
|
||||
User,
|
||||
AuthInfo,
|
||||
AuthToken,
|
||||
AuthIdentity,
|
||||
Worklog,
|
||||
Timefix,
|
||||
WeeklyWorktime,
|
||||
Vacation,
|
||||
Sick,
|
||||
Timewish,
|
||||
Invitation,
|
||||
Watcher
|
||||
} = database.getModels();
|
||||
|
||||
return database.sequelize.transaction(async (transaction) => {
|
||||
const user = await User.findByPk(userId, { transaction });
|
||||
if (!user) {
|
||||
throw new Error('Benutzer nicht gefunden');
|
||||
}
|
||||
|
||||
const authInfo = await AuthInfo.findOne({
|
||||
where: { user_id: userId },
|
||||
transaction
|
||||
});
|
||||
|
||||
if (authInfo) {
|
||||
await AuthToken.destroy({ where: { auth_info_id: authInfo.id }, transaction });
|
||||
await AuthIdentity.destroy({ where: { auth_info_id: authInfo.id }, transaction });
|
||||
}
|
||||
|
||||
await this.destroyOptional(Watcher, { user_id: userId }, transaction);
|
||||
await this.destroyOptional(Invitation, { inviter_user_id: userId }, transaction);
|
||||
|
||||
await WeeklyWorktime.destroy({ where: { user_id: userId }, transaction });
|
||||
await Timewish.destroy({ where: { user_id: userId }, transaction });
|
||||
await Vacation.destroy({ where: { user_id: userId }, transaction });
|
||||
await Sick.destroy({ where: { user_id: userId }, transaction });
|
||||
await Timefix.destroy({ where: { user_id: userId }, transaction });
|
||||
|
||||
await Worklog.destroy({
|
||||
where: {
|
||||
user_id: userId,
|
||||
relatedTo_id: { [Op.ne]: null }
|
||||
},
|
||||
transaction
|
||||
});
|
||||
await Worklog.destroy({ where: { user_id: userId }, transaction });
|
||||
|
||||
if (authInfo) {
|
||||
await AuthInfo.destroy({ where: { id: authInfo.id }, transaction });
|
||||
}
|
||||
|
||||
await User.destroy({ where: { id: userId }, transaction });
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Benutzer-Profil abrufen
|
||||
* @param {number} userId - Benutzer-ID
|
||||
@@ -700,4 +781,3 @@ ${frontendUrl}
|
||||
}
|
||||
|
||||
module.exports = new AuthService();
|
||||
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 1.1 MiB After Width: | Height: | Size: 176 KiB |
@@ -78,6 +78,8 @@ const pageTitle = computed(() => {
|
||||
'settings-timewish': 'Zeitwünsche',
|
||||
'settings-invite': 'Einladen',
|
||||
'settings-permissions': 'Berechtigungen',
|
||||
'privacy': 'Datenschutz',
|
||||
'account-deletion': 'Account löschen',
|
||||
'export': 'Export',
|
||||
'entries': 'Einträge',
|
||||
'stats': 'Statistiken'
|
||||
@@ -280,4 +282,3 @@ const pageTitle = computed(() => {
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
</style>
|
||||
|
||||
|
||||
@@ -82,7 +82,9 @@ const SECTIONS_USER = [
|
||||
{ label: 'Paßwort ändern', to: '/settings/password' },
|
||||
{ label: 'Zeitwünsche', to: '/settings/timewish' },
|
||||
{ label: 'Zugriffe verwalten', to: '/settings/permissions' },
|
||||
{ label: 'Einladen', to: '/settings/invite' }
|
||||
{ label: 'Einladen', to: '/settings/invite' },
|
||||
{ label: 'Account löschen', to: '/account-loeschen' },
|
||||
{ label: 'Datenschutz', to: '/datenschutz' }
|
||||
]
|
||||
}
|
||||
]
|
||||
@@ -189,5 +191,3 @@ const toggleSection = (title) => {
|
||||
font-weight: 500;
|
||||
}
|
||||
</style>
|
||||
|
||||
|
||||
|
||||
@@ -9,6 +9,8 @@ import Register from '../views/Register.vue'
|
||||
import PasswordForgot from '../views/PasswordForgot.vue'
|
||||
import PasswordReset from '../views/PasswordReset.vue'
|
||||
import OAuthCallback from '../views/OAuthCallback.vue'
|
||||
import AccountDeletion from '../views/AccountDeletion.vue'
|
||||
import Privacy from '../views/Privacy.vue'
|
||||
import WeekOverview from '../views/WeekOverview.vue'
|
||||
import Timefix from '../views/Timefix.vue'
|
||||
import Vacation from '../views/Vacation.vue'
|
||||
@@ -57,6 +59,16 @@ const router = createRouter({
|
||||
name: 'oauth-callback',
|
||||
component: OAuthCallback
|
||||
},
|
||||
{
|
||||
path: '/datenschutz',
|
||||
name: 'privacy',
|
||||
component: Privacy
|
||||
},
|
||||
{
|
||||
path: '/account-loeschen',
|
||||
name: 'account-deletion',
|
||||
component: AccountDeletion
|
||||
},
|
||||
|
||||
// Geschützte Routes
|
||||
{
|
||||
@@ -192,4 +204,3 @@ router.beforeEach(async (to, from, next) => {
|
||||
})
|
||||
|
||||
export default router
|
||||
|
||||
|
||||
123
frontend/src/views/AccountDeletion.vue
Normal file
123
frontend/src/views/AccountDeletion.vue
Normal file
@@ -0,0 +1,123 @@
|
||||
<template>
|
||||
<div class="legal-page">
|
||||
<div class="legal-card">
|
||||
<AppBrand class="legal-brand" />
|
||||
<h1>Account und Daten löschen</h1>
|
||||
<p class="updated">Stand: 15.05.2026</p>
|
||||
|
||||
<section>
|
||||
<h2>Wie kann ich meinen Account selbst löschen?</h2>
|
||||
<p>
|
||||
Melden Sie sich in der Web-UI an und öffnen Sie
|
||||
<router-link to="/settings/profile">Persönliches</router-link>.
|
||||
Dort befindet sich der Bereich <strong>Account löschen</strong>. Nach Eingabe
|
||||
von <strong>ACCOUNT LÖSCHEN</strong> und Bestätigung wird der Account dauerhaft
|
||||
gelöscht.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h2>Kann ich die Löschung auch ohne App beantragen?</h2>
|
||||
<p>
|
||||
Ja. Senden Sie eine Löschanfrage per E-Mail an
|
||||
<a href="mailto:kontakt@tsschulz.de?subject=TimeClock%20Account%20loeschen">kontakt@tsschulz.de</a>.
|
||||
Bitte nennen Sie die E-Mail-Adresse des TimeClock-Accounts. Zur Vermeidung
|
||||
unberechtigter Löschungen kann eine Bestätigung der Account-Inhaberschaft
|
||||
erforderlich sein.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h2>Welche Daten werden gelöscht?</h2>
|
||||
<p>
|
||||
Gelöscht werden Accountdaten, Anmeldedaten, Google-Verknüpfungen,
|
||||
Zeitbuchungen, Zeitkorrekturen, Urlaubs- und Krankmeldungen,
|
||||
Zeitwünsche, Wochenarbeitszeiten und persönliche Einstellungen.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h2>Welche Daten können vorübergehend verbleiben?</h2>
|
||||
<p>
|
||||
Server-Logs und Backups können technisch bedingt bis zum Ablauf der
|
||||
regulären Löschzyklen weiterbestehen. Daten können außerdem aufbewahrt
|
||||
werden, soweit gesetzliche Pflichten oder die Geltendmachung, Ausübung
|
||||
oder Verteidigung von Rechtsansprüchen dies erfordern.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h2>Wie lange dauert die Bearbeitung?</h2>
|
||||
<p>
|
||||
Eine Löschung über die Web-UI erfolgt unmittelbar. Löschanfragen per E-Mail
|
||||
werden ohne unangemessene Verzögerung bearbeitet. Nutzer erhalten eine
|
||||
Rückmeldung, falls weitere Informationen zur Identitätsprüfung erforderlich sind.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h2>Wo finde ich die Datenschutzerklärung?</h2>
|
||||
<p>
|
||||
Die Datenschutzerklärung ist unter
|
||||
<router-link to="/datenschutz">https://stechuhr3.tsschulz.de/datenschutz</router-link>
|
||||
abrufbar.
|
||||
</p>
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import AppBrand from '../components/AppBrand.vue'
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.legal-page {
|
||||
min-height: 100vh;
|
||||
background: #f7f7f7;
|
||||
padding: 32px 16px;
|
||||
}
|
||||
|
||||
.legal-card {
|
||||
max-width: 900px;
|
||||
margin: 0 auto;
|
||||
background: #fff;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 6px;
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.08);
|
||||
padding: 28px;
|
||||
}
|
||||
|
||||
.legal-brand {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
h1 {
|
||||
margin: 0 0 6px;
|
||||
color: #222;
|
||||
}
|
||||
|
||||
.updated {
|
||||
margin: 0 0 24px;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
section {
|
||||
margin-top: 24px;
|
||||
}
|
||||
|
||||
h2 {
|
||||
font-size: 18px;
|
||||
margin: 0 0 8px;
|
||||
color: #222;
|
||||
}
|
||||
|
||||
p {
|
||||
color: #333;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
a {
|
||||
color: #2f8f3a;
|
||||
}
|
||||
</style>
|
||||
@@ -101,6 +101,10 @@
|
||||
<router-link to="/password-forgot" class="link">Passwort vergessen</router-link>
|
||||
|
|
||||
<router-link to="/register" class="link">Registrieren</router-link>
|
||||
|
|
||||
<router-link to="/account-loeschen" class="link">Account löschen</router-link>
|
||||
|
|
||||
<router-link to="/datenschutz" class="link">Datenschutz</router-link>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
209
frontend/src/views/Privacy.vue
Normal file
209
frontend/src/views/Privacy.vue
Normal file
@@ -0,0 +1,209 @@
|
||||
<template>
|
||||
<div class="legal-page">
|
||||
<div class="legal-card">
|
||||
<AppBrand class="legal-brand" />
|
||||
<h1>Datenschutzerklärung</h1>
|
||||
<p class="updated">Stand: 15.05.2026</p>
|
||||
<p class="notice">
|
||||
Hinweis vor Veröffentlichung: Die ladungsfähige Anschrift des Verantwortlichen
|
||||
muss vor Einreichung im Play Store ergänzt werden.
|
||||
</p>
|
||||
|
||||
<section>
|
||||
<h2>Wer ist verantwortlich?</h2>
|
||||
<p>
|
||||
Verantwortlich für die Datenverarbeitung in TimeClock ist Torsten Schulz.
|
||||
Anschrift: <strong>BITTE VOR VERÖFFENTLICHUNG ERGÄNZEN</strong>.
|
||||
Kontakt: <a href="mailto:kontakt@tsschulz.de">kontakt@tsschulz.de</a>.
|
||||
</p>
|
||||
<p>
|
||||
Ein Datenschutzbeauftragter ist nicht benannt, sofern keine gesetzliche
|
||||
Pflicht zur Benennung besteht.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h2>Wofür wird TimeClock genutzt?</h2>
|
||||
<p>
|
||||
TimeClock ist eine Zeiterfassungsanwendung. Die App verarbeitet Daten zur
|
||||
Anmeldung, Zeiterfassung, Korrektur, Auswertung und Verwaltung persönlicher
|
||||
Arbeitszeiten.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h2>Welche Daten werden verarbeitet?</h2>
|
||||
<ul>
|
||||
<li>Accountdaten: Name, E-Mail-Adresse, Passwort-Hash, Login-Status</li>
|
||||
<li>Zeiterfassungsdaten: Kommen, Gehen, Pausen, Korrekturen, Zeitstempel</li>
|
||||
<li>Abwesenheitsdaten: Urlaub, Krankheit, Feiertage, Arbeitszeitwünsche</li>
|
||||
<li>Einstellungen: Bundesland, Soll-Arbeitszeit, Anzeigeoptionen, Berechtigungen</li>
|
||||
<li>Technische Daten: IP-Adresse, Zugriffszeitpunkt, Geräte-/Browserinformationen, Server-Logs</li>
|
||||
<li>Bei optionaler Google-Anmeldung: Google-Konto-ID, E-Mail-Adresse und Anzeigename</li>
|
||||
</ul>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h2>Auf welchen Rechtsgrundlagen erfolgt die Verarbeitung?</h2>
|
||||
<ul>
|
||||
<li>Art. 6 Abs. 1 lit. b DSGVO für Account, Login und die Bereitstellung der Zeiterfassung</li>
|
||||
<li>Art. 6 Abs. 1 lit. c DSGVO, soweit gesetzliche Aufbewahrungs- oder Nachweispflichten bestehen</li>
|
||||
<li>Art. 6 Abs. 1 lit. f DSGVO für Sicherheit, Fehleranalyse, Missbrauchsschutz und Server-Logs</li>
|
||||
<li>Art. 6 Abs. 1 lit. a DSGVO für optionale Funktionen, soweit eine Einwilligung erforderlich ist</li>
|
||||
</ul>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h2>Wird Google-Anmeldung verwendet?</h2>
|
||||
<p>
|
||||
Ja, optional. Nutzer können sich mit Google anmelden oder ein bestehendes
|
||||
Konto mit Google verknüpfen. Dabei werden nur die für die Anmeldung
|
||||
erforderlichen Profilinformationen verwendet. Das Google-Passwort wird
|
||||
nicht an TimeClock übermittelt oder gespeichert.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h2>Wer erhält Daten?</h2>
|
||||
<p>
|
||||
Personenbezogene Daten werden nicht verkauft. Eine Weitergabe erfolgt nur,
|
||||
soweit sie für Betrieb, Authentifizierung, Hosting, E-Mail-Versand,
|
||||
Fehleranalyse oder gesetzliche Pflichten erforderlich ist. Eingesetzte
|
||||
Kategorien von Empfängern sind Hosting-/Serveranbieter, E-Mail-Dienstleister
|
||||
und Google als OAuth-Anbieter, wenn Google-Anmeldung genutzt wird.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h2>Werden Daten in Drittländer übertragen?</h2>
|
||||
<p>
|
||||
Beim Einsatz von Google-Anmeldung kann eine Verarbeitung durch Google auch
|
||||
außerhalb der EU/des EWR stattfinden. Für den regulären Betrieb der
|
||||
TimeClock-Datenbank ist kein Verkauf oder keine werbliche Weitergabe von
|
||||
Nutzerdaten vorgesehen.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h2>Wie lange werden Daten gespeichert?</h2>
|
||||
<p>
|
||||
Account-, Zeit- und Einstellungsdaten werden grundsätzlich gespeichert,
|
||||
solange der Account besteht. Nach Account-Löschung werden die aktiven
|
||||
Accountdaten, Anmeldedaten, Google-Verknüpfungen, Zeitbuchungen,
|
||||
Korrekturen, Urlaubs- und Krankmeldungen sowie persönliche Einstellungen
|
||||
gelöscht. Server-Logs und Backups können technisch bedingt für einen
|
||||
begrenzten Zeitraum weiterbestehen und werden im Rahmen der regulären
|
||||
Löschzyklen entfernt. Daten können länger aufbewahrt werden, wenn gesetzliche
|
||||
Pflichten oder die Geltendmachung, Ausübung oder Verteidigung von
|
||||
Rechtsansprüchen dies erfordern.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h2>Wie kann der Account gelöscht werden?</h2>
|
||||
<p>
|
||||
Eingeloggte Nutzer können ihren Account in der Web-UI unter
|
||||
<strong>Persönliches</strong> löschen. Zusätzlich steht eine öffentliche
|
||||
Löschanfrage-Seite zur Verfügung:
|
||||
<router-link to="/account-loeschen">Account und Daten löschen</router-link>.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h2>Welche Rechte haben Nutzer?</h2>
|
||||
<p>
|
||||
Nutzer können Auskunft, Berichtigung, Löschung, Einschränkung der
|
||||
Verarbeitung und Datenübertragbarkeit verlangen sowie einer Verarbeitung
|
||||
widersprechen. Außerdem besteht ein Beschwerderecht bei einer zuständigen
|
||||
Datenschutzaufsichtsbehörde. Anfragen können an
|
||||
<a href="mailto:kontakt@tsschulz.de">kontakt@tsschulz.de</a> gerichtet werden.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h2>Wie werden Daten geschützt?</h2>
|
||||
<p>
|
||||
Die Übertragung erfolgt über HTTPS. Passwörter werden nicht im Klartext
|
||||
gespeichert. Zugriffstokens und OAuth-Verknüpfungen werden serverseitig
|
||||
verwaltet. Zugriffe werden auf das erforderliche Maß beschränkt.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h2>Kann sich diese Erklärung ändern?</h2>
|
||||
<p>
|
||||
Ja. Diese Datenschutzerklärung kann angepasst werden, wenn sich Funktionen,
|
||||
technische Abläufe oder rechtliche Anforderungen ändern.
|
||||
</p>
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import AppBrand from '../components/AppBrand.vue'
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.legal-page {
|
||||
min-height: 100vh;
|
||||
background: #f7f7f7;
|
||||
padding: 32px 16px;
|
||||
}
|
||||
|
||||
.legal-card {
|
||||
max-width: 900px;
|
||||
margin: 0 auto;
|
||||
background: #fff;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 6px;
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.08);
|
||||
padding: 28px;
|
||||
}
|
||||
|
||||
.legal-brand {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
h1 {
|
||||
margin: 0 0 6px;
|
||||
color: #222;
|
||||
}
|
||||
|
||||
.updated {
|
||||
margin: 0 0 16px;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.notice {
|
||||
padding: 10px 12px;
|
||||
border: 1px solid #f0ad4e;
|
||||
background: #fcf8e3;
|
||||
border-radius: 4px;
|
||||
color: #6f4e00;
|
||||
}
|
||||
|
||||
section {
|
||||
margin-top: 24px;
|
||||
}
|
||||
|
||||
h2 {
|
||||
font-size: 18px;
|
||||
margin: 0 0 8px;
|
||||
color: #222;
|
||||
}
|
||||
|
||||
p,
|
||||
li {
|
||||
color: #333;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
ul {
|
||||
padding-left: 22px;
|
||||
}
|
||||
|
||||
a {
|
||||
color: #2f8f3a;
|
||||
}
|
||||
</style>
|
||||
@@ -106,6 +106,31 @@
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<section class="danger-zone">
|
||||
<h2>Account löschen</h2>
|
||||
<p>
|
||||
Löscht den Account, die Anmeldung, Google-Verknüpfungen, Zeitbuchungen,
|
||||
Korrekturen, Urlaubs- und Krankmeldungen sowie persönliche Einstellungen dauerhaft.
|
||||
</p>
|
||||
<label for="deleteConfirmation">
|
||||
Zur Bestätigung bitte <strong>ACCOUNT LÖSCHEN</strong> eingeben
|
||||
</label>
|
||||
<input
|
||||
id="deleteConfirmation"
|
||||
v-model="deleteConfirmation"
|
||||
type="text"
|
||||
autocomplete="off"
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-danger"
|
||||
:disabled="deleting || deleteConfirmation !== deletePhrase"
|
||||
@click="deleteAccount"
|
||||
>
|
||||
{{ deleting ? 'Account wird gelöscht...' : 'Account dauerhaft löschen' }}
|
||||
</button>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<!-- Modal-Komponente -->
|
||||
@@ -138,6 +163,9 @@ const authStore = useAuthStore()
|
||||
const availableStates = ref([])
|
||||
const identities = ref([])
|
||||
const loading = ref(false)
|
||||
const deleting = ref(false)
|
||||
const deletePhrase = 'ACCOUNT LÖSCHEN'
|
||||
const deleteConfirmation = ref('')
|
||||
|
||||
const { showModal, modalConfig, alert, confirm, onConfirm, onCancel } = useModal()
|
||||
|
||||
@@ -302,6 +330,35 @@ async function unlinkGoogle() {
|
||||
}
|
||||
}
|
||||
|
||||
async function deleteAccount() {
|
||||
const confirmed = await confirm(
|
||||
'Der Account und alle personenbezogenen Daten werden dauerhaft gelöscht. Diese Aktion kann nicht rückgängig gemacht werden.',
|
||||
'Account dauerhaft löschen'
|
||||
)
|
||||
if (!confirmed) return
|
||||
|
||||
try {
|
||||
deleting.value = true
|
||||
const response = await fetch(`${API_URL}/auth/account`, {
|
||||
method: 'DELETE',
|
||||
headers: authStore.getAuthHeaders(),
|
||||
body: JSON.stringify({ confirmation: deleteConfirmation.value })
|
||||
})
|
||||
const result = await response.json()
|
||||
if (!response.ok || !result.success) {
|
||||
throw new Error(result.error || 'Account konnte nicht gelöscht werden')
|
||||
}
|
||||
|
||||
await alert('Account wurde gelöscht', 'Erfolg')
|
||||
authStore.clearAuth()
|
||||
router.push('/login')
|
||||
} catch (error) {
|
||||
await alert(`Fehler: ${error.message}`, 'Fehler')
|
||||
} finally {
|
||||
deleting.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// Initiales Laden
|
||||
onMounted(async () => {
|
||||
await Promise.all([
|
||||
@@ -408,6 +465,41 @@ onMounted(async () => {
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
.danger-zone {
|
||||
margin-top: 32px;
|
||||
padding-top: 24px;
|
||||
border-top: 1px solid #f0c2c2;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.danger-zone h2 {
|
||||
margin: 0;
|
||||
color: #a94442;
|
||||
font-size: 20px;
|
||||
}
|
||||
|
||||
.danger-zone p {
|
||||
margin: 0;
|
||||
color: #555;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.danger-zone label {
|
||||
font-weight: 600;
|
||||
font-size: 14px;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.danger-zone input {
|
||||
padding: 10px 12px;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 4px;
|
||||
font-size: 14px;
|
||||
font-family: inherit;
|
||||
}
|
||||
|
||||
.btn {
|
||||
padding: 10px 20px;
|
||||
border: none;
|
||||
@@ -434,4 +526,13 @@ onMounted(async () => {
|
||||
transform: translateY(-1px);
|
||||
box-shadow: 0 4px 8px rgba(76, 175, 80, 0.4);
|
||||
}
|
||||
|
||||
.btn-danger {
|
||||
background: #d9534f;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.btn-danger:hover:not(:disabled) {
|
||||
background: #c9302c;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -83,6 +83,8 @@
|
||||
<div class="buttons">
|
||||
Bereits registriert?
|
||||
<router-link to="/login" class="link">Jetzt einloggen</router-link>
|
||||
|
|
||||
<router-link to="/datenschutz" class="link">Datenschutz</router-link>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
@@ -23,8 +23,8 @@ android {
|
||||
applicationId = "de.tsschulz.timeclock"
|
||||
minSdk = 26
|
||||
targetSdk = 36
|
||||
versionCode = 6
|
||||
versionName = "0.8.0-alpha5"
|
||||
versionCode = 7
|
||||
versionName = "0.8.0-alpha6"
|
||||
buildConfigField("String", "API_BASE_URL", "\"${apiBaseUrl.replace("\\", "\\\\").replace("\"", "\\\"")}\"")
|
||||
}
|
||||
|
||||
|
||||
Binary file not shown.
@@ -159,6 +159,7 @@ fun TimeClockApp(
|
||||
settingsViewModel = settingsViewModel,
|
||||
adminState = adminState,
|
||||
adminViewModel = adminViewModel,
|
||||
openUrl = openUrl,
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -181,6 +182,7 @@ private fun DemoScreen(
|
||||
settingsViewModel: SettingsViewModel,
|
||||
adminState: de.tsschulz.timeclock.ui.admin.AdminUiState,
|
||||
adminViewModel: AdminViewModel,
|
||||
openUrl: (String) -> Unit,
|
||||
) {
|
||||
when (route) {
|
||||
AppRoute.Week -> WeekOverviewScreen(
|
||||
@@ -289,6 +291,10 @@ private fun DemoScreen(
|
||||
onSend = { settingsViewModel.sendInvite(it) },
|
||||
)
|
||||
}
|
||||
AppRoute.AccountDeletion -> AccountDeletionScreen(
|
||||
onOpenDeletionPage = { openUrl("https://stechuhr3.tsschulz.de/account-loeschen") }
|
||||
)
|
||||
AppRoute.Privacy -> PrivacyScreen()
|
||||
AppRoute.Holidays -> HolidaysAdminScreen(
|
||||
state = adminState,
|
||||
isTablet = isTablet,
|
||||
@@ -302,6 +308,79 @@ private fun DemoScreen(
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun AccountDeletionScreen(onOpenDeletionPage: () -> Unit) {
|
||||
TcCard {
|
||||
SectionTitle("Account und Daten löschen")
|
||||
PrivacySection(
|
||||
"Wie lösche ich meinen Account?",
|
||||
"Die vollständige Account-Löschung erfolgt in der Web-UI unter Persönliches. Dort werden Accountdaten, Anmeldedaten, Google-Verknüpfungen, Zeitbuchungen, Korrekturen, Urlaubs- und Krankmeldungen sowie persönliche Einstellungen gelöscht."
|
||||
)
|
||||
PrivacySection(
|
||||
"Kann ich die Löschung ohne App beantragen?",
|
||||
"Ja. Die öffentliche Löschseite enthält die Schritte und eine Kontaktmöglichkeit per E-Mail."
|
||||
)
|
||||
Row(modifier = Modifier.padding(top = TcSpacing.Lg)) {
|
||||
TcButton(
|
||||
text = "Löschseite öffnen",
|
||||
variant = ButtonVariant.Danger,
|
||||
onClick = onOpenDeletionPage,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun PrivacyScreen() {
|
||||
TcCard {
|
||||
SectionTitle("Datenschutzerklärung")
|
||||
Text("Stand: 15.05.2026", color = TcColors.TextMuted, fontSize = 13.sp)
|
||||
PrivacySection(
|
||||
"Verantwortlicher",
|
||||
"Verantwortlich für die Datenverarbeitung in TimeClock ist Torsten Schulz. Anschrift: BITTE VOR VERÖFFENTLICHUNG ERGÄNZEN. Kontakt: kontakt@tsschulz.de."
|
||||
)
|
||||
PrivacySection(
|
||||
"Zweck der App",
|
||||
"TimeClock ist eine Zeiterfassungsanwendung. Die App verarbeitet Daten zur Anmeldung, Zeiterfassung, Auswertung und Verwaltung persönlicher Arbeitszeiten."
|
||||
)
|
||||
PrivacySection(
|
||||
"Welche Daten werden verarbeitet?",
|
||||
"Verarbeitet werden Accountdaten, Zeitbuchungen, Korrekturen, Urlaub, Krankheit, Feiertage, Arbeitszeitwünsche, Einstellungen, technische Zugriffsdaten und bei Google-Anmeldung die von Google übermittelte Konto-ID, E-Mail-Adresse und der Anzeigename."
|
||||
)
|
||||
PrivacySection(
|
||||
"Rechtsgrundlagen",
|
||||
"Die Verarbeitung erfolgt insbesondere zur Bereitstellung der Zeiterfassung, zur Erfüllung gesetzlicher Pflichten sowie für Sicherheit, Fehleranalyse und Missbrauchsschutz."
|
||||
)
|
||||
PrivacySection(
|
||||
"Google-Anmeldung",
|
||||
"Die Google-Anmeldung ist optional. Das Google-Passwort wird nicht an TimeClock übermittelt oder gespeichert."
|
||||
)
|
||||
PrivacySection(
|
||||
"Speicherung und Löschung",
|
||||
"Daten werden gespeichert, solange der Account besteht oder gesetzliche Aufbewahrungspflichten bestehen. Nutzer können ihren Account in der Web-UI unter Persönliches löschen oder eine Löschung über https://stechuhr3.tsschulz.de/account-loeschen beantragen."
|
||||
)
|
||||
PrivacySection(
|
||||
"Rechte der Nutzer",
|
||||
"Nutzer können Auskunft, Berichtigung, Löschung, Einschränkung der Verarbeitung und Datenübertragbarkeit verlangen sowie einer Verarbeitung widersprechen. Anfragen können an kontakt@tsschulz.de gerichtet werden."
|
||||
)
|
||||
PrivacySection(
|
||||
"Sicherheit",
|
||||
"Die Übertragung erfolgt über HTTPS. Passwörter werden nicht im Klartext gespeichert. Zugriffstokens und OAuth-Verknüpfungen werden serverseitig verwaltet."
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun PrivacySection(title: String, body: String) {
|
||||
Column(
|
||||
modifier = Modifier.padding(top = TcSpacing.Lg),
|
||||
verticalArrangement = Arrangement.spacedBy(TcSpacing.Xs),
|
||||
) {
|
||||
Text(title, color = TcColors.Text, fontSize = 16.sp, fontWeight = FontWeight.SemiBold)
|
||||
Text(body, color = TcColors.Text, fontSize = 14.sp, lineHeight = 20.sp)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun WeekOverviewScreen(
|
||||
week: WeekOverviewDto?,
|
||||
|
||||
@@ -20,6 +20,8 @@ val userSections = listOf(
|
||||
MenuItem("Zeitwünsche", AppRoute.Timewish),
|
||||
MenuItem("Zugriffe verwalten", AppRoute.Permissions),
|
||||
MenuItem("Einladen", AppRoute.Invite),
|
||||
MenuItem("Account löschen", AppRoute.AccountDeletion),
|
||||
MenuItem("Datenschutz", AppRoute.Privacy),
|
||||
),
|
||||
),
|
||||
MenuSection(
|
||||
|
||||
@@ -25,6 +25,8 @@ enum class AppRoute(val title: String) {
|
||||
Timewish("Zeitwünsche"),
|
||||
Permissions("Zugriffe verwalten"),
|
||||
Invite("Einladen"),
|
||||
AccountDeletion("Account löschen"),
|
||||
Privacy("Datenschutz"),
|
||||
Holidays("Feiertage"),
|
||||
Roles("Rechte"),
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user