mirror of https://github.com/calzoneman/sync.git
Implement self-service account deletion
This commit is contained in:
parent
37c6fa3f79
commit
c5c88264f7
|
@ -39,3 +39,28 @@ This email address is not monitored for replies. For assistance with password r
|
||||||
|
|
||||||
from = "Example Website <website@example.com>"
|
from = "Example Website <website@example.com>"
|
||||||
subject = "Password reset request"
|
subject = "Password reset request"
|
||||||
|
|
||||||
|
# Email configuration for account deletion request notifications
|
||||||
|
[delete-account]
|
||||||
|
enabled = true
|
||||||
|
|
||||||
|
html-template = """
|
||||||
|
Hi $user$,
|
||||||
|
<br>
|
||||||
|
<br>
|
||||||
|
Account deletion was requested for your account on CHANGE ME. Your account will be automatically deleted in 7 days without any further action from you.
|
||||||
|
<br>
|
||||||
|
<br>
|
||||||
|
This email address is not monitored for replies. For assistance, please <a href="http://example.com/contact">contact an administrator</a>.
|
||||||
|
"""
|
||||||
|
|
||||||
|
text-template = """
|
||||||
|
Hi $user$,
|
||||||
|
|
||||||
|
Account deletion was requested for your account on CHANGE ME. Your account will be automatically deleted in 7 days without any further action from you.
|
||||||
|
|
||||||
|
This email address is not monitored for replies. For assistance, please contact an administrator. See http://example.com/contact for contact information.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from = "Example Website <website@example.com>"
|
||||||
|
subject = "Account deletion request"
|
||||||
|
|
|
@ -14,7 +14,7 @@ const LOGGER = require('@calzoneman/jsli')('bgtask');
|
||||||
var init = null;
|
var init = null;
|
||||||
|
|
||||||
/* Alias cleanup */
|
/* Alias cleanup */
|
||||||
function initAliasCleanup(_Server) {
|
function initAliasCleanup() {
|
||||||
var CLEAN_INTERVAL = parseInt(Config.get("aliases.purge-interval"));
|
var CLEAN_INTERVAL = parseInt(Config.get("aliases.purge-interval"));
|
||||||
var CLEAN_EXPIRE = parseInt(Config.get("aliases.max-age"));
|
var CLEAN_EXPIRE = parseInt(Config.get("aliases.max-age"));
|
||||||
|
|
||||||
|
@ -28,7 +28,7 @@ function initAliasCleanup(_Server) {
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Password reset cleanup */
|
/* Password reset cleanup */
|
||||||
function initPasswordResetCleanup(_Server) {
|
function initPasswordResetCleanup() {
|
||||||
var CLEAN_INTERVAL = 8*60*60*1000;
|
var CLEAN_INTERVAL = 8*60*60*1000;
|
||||||
|
|
||||||
setInterval(function () {
|
setInterval(function () {
|
||||||
|
@ -74,6 +74,25 @@ function initChannelDumper(Server) {
|
||||||
}, CHANNEL_SAVE_INTERVAL);
|
}, CHANNEL_SAVE_INTERVAL);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function initAccountCleanup() {
|
||||||
|
setInterval(() => {
|
||||||
|
(async () => {
|
||||||
|
let rows = await db.users.findAccountsPendingDeletion();
|
||||||
|
for (let row of rows) {
|
||||||
|
try {
|
||||||
|
await db.users.purgeAccount(row.id);
|
||||||
|
LOGGER.info('Purged account from request %j', row);
|
||||||
|
} catch (error) {
|
||||||
|
LOGGER.error('Error purging account %j: %s', row, error.stack);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})().catch(error => {
|
||||||
|
LOGGER.error('Error purging deleted accounts: %s', error.stack);
|
||||||
|
});
|
||||||
|
//}, 3600 * 1000);
|
||||||
|
}, 60 * 1000);
|
||||||
|
}
|
||||||
|
|
||||||
module.exports = function (Server) {
|
module.exports = function (Server) {
|
||||||
if (init === Server) {
|
if (init === Server) {
|
||||||
LOGGER.warn("Attempted to re-init background tasks");
|
LOGGER.warn("Attempted to re-init background tasks");
|
||||||
|
@ -81,7 +100,8 @@ module.exports = function (Server) {
|
||||||
}
|
}
|
||||||
|
|
||||||
init = Server;
|
init = Server;
|
||||||
initAliasCleanup(Server);
|
initAliasCleanup();
|
||||||
initChannelDumper(Server);
|
initChannelDumper(Server);
|
||||||
initPasswordResetCleanup(Server);
|
initPasswordResetCleanup();
|
||||||
|
initAccountCleanup();
|
||||||
};
|
};
|
||||||
|
|
|
@ -47,6 +47,29 @@ class EmailConfig {
|
||||||
return reset.subject;
|
return reset.subject;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const deleteAccount = config['delete-account'];
|
||||||
|
this._delete = {
|
||||||
|
isEnabled() {
|
||||||
|
return deleteAccount !== null && deleteAccount.enabled;
|
||||||
|
},
|
||||||
|
|
||||||
|
getHTML() {
|
||||||
|
return deleteAccount['html-template'];
|
||||||
|
},
|
||||||
|
|
||||||
|
getText() {
|
||||||
|
return deleteAccount['text-template'];
|
||||||
|
},
|
||||||
|
|
||||||
|
getFrom() {
|
||||||
|
return deleteAccount.from;
|
||||||
|
},
|
||||||
|
|
||||||
|
getSubject() {
|
||||||
|
return deleteAccount.subject;
|
||||||
|
}
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
getSmtp() {
|
getSmtp() {
|
||||||
|
@ -56,6 +79,10 @@ class EmailConfig {
|
||||||
getPasswordReset() {
|
getPasswordReset() {
|
||||||
return this._reset;
|
return this._reset;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getDeleteAccount() {
|
||||||
|
return this._delete;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export { EmailConfig };
|
export { EmailConfig };
|
||||||
|
|
|
@ -26,6 +26,27 @@ class EmailController {
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async sendAccountDeletion(params = {}) {
|
||||||
|
const { address, username } = params;
|
||||||
|
|
||||||
|
const deleteConfig = this.config.getDeleteAccount();
|
||||||
|
|
||||||
|
const html = deleteConfig.getHTML()
|
||||||
|
.replace(/\$user\$/g, username);
|
||||||
|
const text = deleteConfig.getText()
|
||||||
|
.replace(/\$user\$/g, username);
|
||||||
|
|
||||||
|
const result = await this.mailer.sendMail({
|
||||||
|
from: deleteConfig.getFrom(),
|
||||||
|
to: `${username} <${address}>`,
|
||||||
|
subject: deleteConfig.getSubject(),
|
||||||
|
html,
|
||||||
|
text
|
||||||
|
});
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export { EmailController };
|
export { EmailController };
|
||||||
|
|
|
@ -2,6 +2,7 @@ var $util = require("../utilities");
|
||||||
var bcrypt = require("bcrypt");
|
var bcrypt = require("bcrypt");
|
||||||
var db = require("../database");
|
var db = require("../database");
|
||||||
var Config = require("../config");
|
var Config = require("../config");
|
||||||
|
import { promisify } from "bluebird";
|
||||||
|
|
||||||
const LOGGER = require('@calzoneman/jsli')('database/accounts');
|
const LOGGER = require('@calzoneman/jsli')('database/accounts');
|
||||||
|
|
||||||
|
@ -89,7 +90,9 @@ module.exports = {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
db.query("SELECT * FROM `users` WHERE name = ?", [name], function (err, rows) {
|
db.query("SELECT * FROM `users` WHERE name = ? AND inactive = FALSE",
|
||||||
|
[name],
|
||||||
|
function (err, rows) {
|
||||||
if (err) {
|
if (err) {
|
||||||
callback(err, true);
|
callback(err, true);
|
||||||
return;
|
return;
|
||||||
|
@ -244,7 +247,7 @@ module.exports = {
|
||||||
the hashes match.
|
the hashes match.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
db.query("SELECT * FROM `users` WHERE name=?",
|
db.query("SELECT * FROM `users` WHERE name=? AND inactive = FALSE",
|
||||||
[name],
|
[name],
|
||||||
function (err, rows) {
|
function (err, rows) {
|
||||||
if (err) {
|
if (err) {
|
||||||
|
@ -401,7 +404,7 @@ module.exports = {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
db.query("SELECT email FROM `users` WHERE name=?", [name],
|
db.query("SELECT email FROM `users` WHERE name=? AND inactive = FALSE", [name],
|
||||||
function (err, rows) {
|
function (err, rows) {
|
||||||
if (err) {
|
if (err) {
|
||||||
callback(err, null);
|
callback(err, null);
|
||||||
|
@ -519,17 +522,6 @@ module.exports = {
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
|
||||||
* Retrieve a list of channels owned by a user
|
|
||||||
*/
|
|
||||||
getChannels: function (name, callback) {
|
|
||||||
if (typeof callback !== "function") {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
db.query("SELECT * FROM `channels` WHERE owner=?", [name], callback);
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Retrieves all names registered from a given IP
|
* Retrieves all names registered from a given IP
|
||||||
*/
|
*/
|
||||||
|
@ -540,5 +532,57 @@ module.exports = {
|
||||||
|
|
||||||
db.query("SELECT name,global_rank FROM `users` WHERE `ip`=?", [ip],
|
db.query("SELECT name,global_rank FROM `users` WHERE `ip`=?", [ip],
|
||||||
callback);
|
callback);
|
||||||
|
},
|
||||||
|
|
||||||
|
requestAccountDeletion: id => {
|
||||||
|
return db.getDB().runTransaction(async tx => {
|
||||||
|
try {
|
||||||
|
let user = await tx.table('users').where({ id }).first();
|
||||||
|
await tx.table('user_deletion_requests')
|
||||||
|
.insert({
|
||||||
|
user_id: id
|
||||||
|
});
|
||||||
|
await tx.table('users')
|
||||||
|
.where({ id })
|
||||||
|
.update({ password: '', inactive: true });
|
||||||
|
|
||||||
|
// TODO: ideally password reset should be by user_id and not name
|
||||||
|
// For now, we need to make sure to clear it
|
||||||
|
await tx.table('password_reset')
|
||||||
|
.where({ name: user.name })
|
||||||
|
.delete();
|
||||||
|
} catch (error) {
|
||||||
|
// Ignore unique violation -- probably caused by a duplicate request
|
||||||
|
if (error.code !== 'ER_DUP_ENTRY') {
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
findAccountsPendingDeletion: () => {
|
||||||
|
return db.getDB().runTransaction(tx => {
|
||||||
|
let lastWeek = new Date(Date.now() - 7 * 24 * 3600 * 1000);
|
||||||
|
return tx.table('user_deletion_requests')
|
||||||
|
.where('user_deletion_requests.created_at', '<', lastWeek)
|
||||||
|
.join('users', 'user_deletion_requests.user_id', '=', 'users.id')
|
||||||
|
.select('users.id', 'users.name');
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
purgeAccount: id => {
|
||||||
|
return db.getDB().runTransaction(async tx => {
|
||||||
|
let user = await tx.table('users').where({ id }).first();
|
||||||
|
if (!user) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
await tx.table('channel_ranks').where({ name: user.name }).delete();
|
||||||
|
await tx.table('user_playlists').where({ user: user.name }).delete();
|
||||||
|
await tx.table('users').where({ id }).delete();
|
||||||
|
return true;
|
||||||
|
});
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
module.exports.verifyLoginAsync = promisify(module.exports.verifyLogin);
|
||||||
|
|
|
@ -230,6 +230,18 @@ module.exports = {
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
|
listUserChannelsAsync: owner => {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
module.exports.listUserChannels(owner, (error, rows) => {
|
||||||
|
if (error) {
|
||||||
|
reject(error);
|
||||||
|
} else {
|
||||||
|
resolve(rows);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Loads the channel from the database
|
* Loads the channel from the database
|
||||||
*/
|
*/
|
||||||
|
|
|
@ -129,4 +129,16 @@ export async function initTables() {
|
||||||
t.index(['ip', 'channel']);
|
t.index(['ip', 'channel']);
|
||||||
t.index(['name', 'channel']);
|
t.index(['name', 'channel']);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
await ensureTable('user_deletion_requests', t => {
|
||||||
|
t.increments('request_id').notNullable().primary();
|
||||||
|
t.integer('user_id')
|
||||||
|
.unsigned()
|
||||||
|
.notNullable()
|
||||||
|
.references('id').inTable('users')
|
||||||
|
.onDelete('cascade')
|
||||||
|
.unique();
|
||||||
|
t.timestamps(/* useTimestamps */ true, /* defaultToNow */ true);
|
||||||
|
t.index('created_at');
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,7 +3,7 @@ import Promise from 'bluebird';
|
||||||
|
|
||||||
const LOGGER = require('@calzoneman/jsli')('database/update');
|
const LOGGER = require('@calzoneman/jsli')('database/update');
|
||||||
|
|
||||||
const DB_VERSION = 11;
|
const DB_VERSION = 12;
|
||||||
var hasUpdates = [];
|
var hasUpdates = [];
|
||||||
|
|
||||||
module.exports.checkVersion = function () {
|
module.exports.checkVersion = function () {
|
||||||
|
@ -51,6 +51,8 @@ function update(version, cb) {
|
||||||
addChannelLastLoadedColumn(cb);
|
addChannelLastLoadedColumn(cb);
|
||||||
} else if (version < 11) {
|
} else if (version < 11) {
|
||||||
addChannelOwnerLastSeenColumn(cb);
|
addChannelOwnerLastSeenColumn(cb);
|
||||||
|
} else if (version < 12) {
|
||||||
|
addUserInactiveColumn(cb);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -128,3 +130,14 @@ function addChannelOwnerLastSeenColumn(cb) {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function addUserInactiveColumn(cb) {
|
||||||
|
db.query("ALTER TABLE users ADD COLUMN inactive BOOLEAN DEFAULT FALSE", error => {
|
||||||
|
if (error) {
|
||||||
|
LOGGER.error(`Failed to add inactive column: ${error}`);
|
||||||
|
cb(error);
|
||||||
|
} else {
|
||||||
|
cb();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
|
@ -0,0 +1,161 @@
|
||||||
|
import { sendPug } from '../../pug';
|
||||||
|
import Config from '../../../config';
|
||||||
|
import { eventlog } from '../../../logger';
|
||||||
|
const verifySessionAsync = require('bluebird').promisify(
|
||||||
|
require('../../../session').verifySession
|
||||||
|
);
|
||||||
|
|
||||||
|
const LOGGER = require('@calzoneman/jsli')('web/routes/account/delete-account');
|
||||||
|
|
||||||
|
export default function initialize(
|
||||||
|
app,
|
||||||
|
csrfVerify,
|
||||||
|
channelDb,
|
||||||
|
userDb,
|
||||||
|
emailConfig,
|
||||||
|
emailController
|
||||||
|
) {
|
||||||
|
app.get('/account/delete', async (req, res) => {
|
||||||
|
if (!await authorize(req, res)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await showDeletePage(res, {});
|
||||||
|
});
|
||||||
|
|
||||||
|
app.post('/account/delete', async (req, res) => {
|
||||||
|
if (!await authorize(req, res)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
csrfVerify(req);
|
||||||
|
|
||||||
|
if (!req.body.confirmed) {
|
||||||
|
await showDeletePage(res, { missingConfirmation: true });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let user;
|
||||||
|
try {
|
||||||
|
user = await userDb.verifyLoginAsync(res.locals.loginName, req.body.password);
|
||||||
|
} catch (error) {
|
||||||
|
if (error.message === 'Invalid username/password combination') {
|
||||||
|
res.status(403);
|
||||||
|
await showDeletePage(res, { wrongPassword: true });
|
||||||
|
} else if (error.message === 'User does not exist' ||
|
||||||
|
error.message.match(/Invalid username/)) {
|
||||||
|
LOGGER.error('User does not exist after authorization');
|
||||||
|
res.status(503);
|
||||||
|
await showDeletePage(res, { internalError: true });
|
||||||
|
} else {
|
||||||
|
res.status(503);
|
||||||
|
LOGGER.error('Unknown error in verifyLogin: %s', error.stack);
|
||||||
|
await showDeletePage(res, { internalError: true });
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
let channels = await channelDb.listUserChannelsAsync(user.name);
|
||||||
|
if (channels.length > 0) {
|
||||||
|
await showDeletePage(res, { channelCount: channels.length });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
LOGGER.error('Unknown error in listUserChannels: %s', error.stack);
|
||||||
|
await showDeletePage(res, { internalError: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await userDb.requestAccountDeletion(user.id);
|
||||||
|
eventlog.log(`[account] ${req.ip} requested account deletion for ${user.name}`);
|
||||||
|
} catch (error) {
|
||||||
|
LOGGER.error('Unknown error in requestAccountDeletion: %s', error.stack);
|
||||||
|
await showDeletePage(res, { internalError: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (emailConfig.getDeleteAccount().isEnabled() && user.email) {
|
||||||
|
await sendEmail(user);
|
||||||
|
} else {
|
||||||
|
LOGGER.warn(
|
||||||
|
'Skipping account deletion email notification for %s',
|
||||||
|
user.name
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
res.clearCookie('auth', { domain: Config.get('http.root-domain-dotted') });
|
||||||
|
res.locals.loggedIn = false;
|
||||||
|
res.locals.loginName = null;
|
||||||
|
sendPug(
|
||||||
|
res,
|
||||||
|
'account-deleted',
|
||||||
|
{}
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
async function showDeletePage(res, flags) {
|
||||||
|
let locals = Object.assign({ channelCount: 0 }, flags);
|
||||||
|
|
||||||
|
if (res.locals.loggedIn) {
|
||||||
|
let channels = await channelDb.listUserChannelsAsync(
|
||||||
|
res.locals.loginName
|
||||||
|
);
|
||||||
|
locals.channelCount = channels.length;
|
||||||
|
} else {
|
||||||
|
res.status(401);
|
||||||
|
}
|
||||||
|
|
||||||
|
sendPug(
|
||||||
|
res,
|
||||||
|
'account-delete',
|
||||||
|
locals
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function authorize(req, res) {
|
||||||
|
try {
|
||||||
|
if (!res.locals.loggedIn) {
|
||||||
|
res.status(401);
|
||||||
|
await showDeletePage(res, {});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!req.signedCookies || !req.signedCookies.auth) {
|
||||||
|
throw new Error('Missing auth cookie');
|
||||||
|
}
|
||||||
|
|
||||||
|
await verifySessionAsync(req.signedCookies.auth);
|
||||||
|
return true;
|
||||||
|
} catch (error) {
|
||||||
|
res.status(401);
|
||||||
|
sendPug(
|
||||||
|
res,
|
||||||
|
'account-delete',
|
||||||
|
{ authFailed: true, reason: error.message }
|
||||||
|
);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function sendEmail(user) {
|
||||||
|
LOGGER.info(
|
||||||
|
'Sending email notification for account deletion %s <%s>',
|
||||||
|
user.name,
|
||||||
|
user.email
|
||||||
|
);
|
||||||
|
|
||||||
|
try {
|
||||||
|
await emailController.sendAccountDeletion({
|
||||||
|
username: user.name,
|
||||||
|
address: user.email
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
LOGGER.error(
|
||||||
|
'Sending email notification failed for %s <%s>: %s',
|
||||||
|
user.name,
|
||||||
|
user.email,
|
||||||
|
error.stack
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -203,6 +203,15 @@ module.exports = {
|
||||||
require('./routes/contact')(app, webConfig);
|
require('./routes/contact')(app, webConfig);
|
||||||
require('./auth').init(app);
|
require('./auth').init(app);
|
||||||
require('./account').init(app, globalMessageBus, emailConfig, emailController);
|
require('./account').init(app, globalMessageBus, emailConfig, emailController);
|
||||||
|
require('./routes/account/delete-account')(
|
||||||
|
app,
|
||||||
|
csrf.verify,
|
||||||
|
require('../database/channels'),
|
||||||
|
require('../database/accounts'),
|
||||||
|
emailConfig,
|
||||||
|
emailController
|
||||||
|
);
|
||||||
|
|
||||||
require('./acp').init(app, ioConfig);
|
require('./acp').init(app, ioConfig);
|
||||||
require('../google2vtt').attach(app);
|
require('../google2vtt').attach(app);
|
||||||
require('./routes/google_drive_userscript')(app);
|
require('./routes/google_drive_userscript')(app);
|
||||||
|
|
|
@ -0,0 +1,51 @@
|
||||||
|
extends layout.pug
|
||||||
|
|
||||||
|
block content
|
||||||
|
.col-lg-6.col-lg-offset-3.col-md-6.col-md-offset-3
|
||||||
|
if internalError
|
||||||
|
h2 Error
|
||||||
|
p
|
||||||
|
| Your account deletion request could not be processed due to an internal
|
||||||
|
| error. Please try again later and ask an administrator for assistance
|
||||||
|
| if the problem persists.
|
||||||
|
else if !loggedIn
|
||||||
|
h2 Authentication Required
|
||||||
|
p
|
||||||
|
| You must
|
||||||
|
a(href="/login") log in
|
||||||
|
| before requesting deletion of your account.
|
||||||
|
else if authFailed
|
||||||
|
h2 Authentication failed
|
||||||
|
p= reason
|
||||||
|
else if channelCount > 0
|
||||||
|
h2 Delete Account
|
||||||
|
p
|
||||||
|
| Your account cannot be deleted because you have one or more channels
|
||||||
|
| registered. In order to delete your account, you must first
|
||||||
|
a(href="/account/channels") delete them
|
||||||
|
| or ask an administrator to transfer ownership of these channels
|
||||||
|
| to another account.
|
||||||
|
else
|
||||||
|
h2 Delete Account
|
||||||
|
p
|
||||||
|
strong Submitting this form will initiate permanent deletion of your account.
|
||||||
|
| After 7 days, your account will be permanently deleted and unrecoverable.
|
||||||
|
| During this time, you will not be able to log in, but you can ask an
|
||||||
|
| administrator to restore your account if the deletion was requested in error.
|
||||||
|
| Please confirm your password to continue.
|
||||||
|
form(action="/account/delete", method="post")
|
||||||
|
input(type="hidden", name="_csrf", value=csrfToken)
|
||||||
|
.form-group(class=wrongPassword ? "has-error" : "")
|
||||||
|
label.control-label(for="password") Password
|
||||||
|
input#password.form-control(type="password", name="password")
|
||||||
|
if wrongPassword
|
||||||
|
p.text-danger.
|
||||||
|
Password was incorrect
|
||||||
|
.checkbox
|
||||||
|
label
|
||||||
|
input#confirm-delete(type="checkbox", name="confirmed")
|
||||||
|
| I acknowledge that by submitting this request, my account will be permanently deleted unrecoverably
|
||||||
|
if missingConfirmation
|
||||||
|
p.text-danger.
|
||||||
|
You must check the box to confirm you want to delete your account
|
||||||
|
button.btn.btn-danger.btn-block(type="submit") Delete Account
|
|
@ -0,0 +1,11 @@
|
||||||
|
extends layout.pug
|
||||||
|
|
||||||
|
block content
|
||||||
|
.col-lg-6.col-lg-offset-3.col-md-6.col-md-offset-3
|
||||||
|
h2 Account Deleted
|
||||||
|
p.
|
||||||
|
Your account has been flagged for deletion. After 7 days, your user data
|
||||||
|
will be premanently deleted from the database. During this time, you will
|
||||||
|
not be able to log in, but you can ask an administrator for help if your
|
||||||
|
deletion request was in error. After 7 days, your account will no longer
|
||||||
|
be recoverable.
|
|
@ -19,6 +19,7 @@ mixin navdefaultlinks()
|
||||||
li: a(href="/account/channels") Channels
|
li: a(href="/account/channels") Channels
|
||||||
li: a(href="/account/profile") Profile
|
li: a(href="/account/profile") Profile
|
||||||
li: a(href="/account/edit") Change Password/Email
|
li: a(href="/account/edit") Change Password/Email
|
||||||
|
li: a(href="/account/delete") Delete Account
|
||||||
else
|
else
|
||||||
li: a(href="/login") Login
|
li: a(href="/login") Login
|
||||||
li: a(href="/register") Register
|
li: a(href="/register") Register
|
||||||
|
|
Loading…
Reference in New Issue