Compare commits

...

10 Commits

11 changed files with 363 additions and 38 deletions

View File

@ -2,7 +2,7 @@
irclogger_web is a highly configurable IRC logger with web frontend. It's written in Perl.
## Installation
1. You need to have Perl and SQLite3 interpreter installed.
1. You need to have Perl interpreter and SQLite3 installed.
2. Following Perl packages have to be installed: [DBI](https://metacpan.org/pod/DBI), [DBD::SQLite](https://metacpan.org/pod/DBD::SQLite).
3. Run `./prepare_database.sh` to create SQLite3 database for storing users, servers and logged channels.
4. See `configuration.pm` and adjust it to your needs.

View File

@ -1,7 +1,8 @@
create table channels(id int primary key not null,
server_id int not null, -- foreign key in servers table
name text not null,
public int not null
public int not null,
enabled int not null
);
create table users(id int primary key not null,
@ -13,7 +14,8 @@ create table users(id int primary key not null,
create table servers(id int primary key not null,
name text not null,
host text not null,
port int not null
port int not null,
enabled int not null
);
create table accessors(user_id int not null, -- foreign key in users table

View File

@ -495,6 +495,16 @@ sub createUser {
$query->execute($aName, $password, $aPrivileges);
}
sub deleteUser {
my $aID = $_[0];
my $aConnection = $_[1];
my $query = $aConnection->prepare(qq(delete from users where id=?;));
$query->execute($aID);
$query = $aConnection->prepare(qq(delete from accessors where user_id=?;));
$query->execute($aID);
}
sub httpServerWorker {
my $db = DBI->connect("DBI:SQLite:dbname=$configuration::database", "", "", {RaiseError=>1});
my $query = $db->prepare(qq(select id from users;));

View File

@ -86,6 +86,21 @@ sub verifyChannelAccess {
return 1;
}
sub enumerateServers {
my $aConnection = $_[0];
my $output = "<select name=\"server\">";
my $query = $aConnection->prepare(qq(select id, name from servers;));
$query->execute();
while(my @row = $query->fetchrow_array()) {
my $serverID = $row[0];
my $server = $row[1];
$output.="<option value=\"$serverID\">$server</option>";
}
$output.="</select>";
return $output;
}
sub enumerateChannels {
my $aConnection = $_[0];
@ -102,6 +117,25 @@ sub enumerateChannels {
return $output;
}
sub enumerateUsers {
my $aConnection = $_[0];
my $aSession = $_[1];
my $output = "<select name=\"user\">";
my $query = $aConnection->prepare(qq(select id, name from users;));
$query->execute();
while(my @row = $query->fetchrow_array()) {
my $id = $row[0];
my $name = $row[1];
if($name eq $aSession->{"username"}) {
next;
}
$output.="<option value=\"$id\">$name</option>";
}
$output.="</select>";
return $output;
}
sub handlePath {
my $aClient = $_[0];
my $aPath = $_[1];
@ -129,16 +163,17 @@ sub handlePath {
$userbar.="</form>";
}
my $query = $aConnection->prepare(qq(select channels.id, channels.name, servers.name from channels inner join servers on channels.server_id=servers.id where channels.public=1;));
my $query = $aConnection->prepare(qq(select channels.id, channels.name, channels.enabled, servers.name, servers.enabled from channels inner join servers on channels.server_id=servers.id where channels.public=1;));
$query->execute();
my $table = "";
while(my @row = $query->fetchrow_array()) {
my $channelID = $row[0];
my $channelName = $row[1];
my $serverName = $row[2];
my $channelEnabled = $row[2] && $row[4];
my $serverName = $row[3];
$channelName =~ s/%23/#/;
$table.="<tr><td><a href=\"view_logs?channel=$channelID\">$channelName</a></td><td>$serverName</td></tr>";
my $status = $channelEnabled?"<span style=\"color:green\">Enabled</span>":"<span style=\"color:gray\">Disabled</span>";
$table.="<tr><td><a href=\"view_logs?channel=$channelID\">$channelName</a></td><td>$serverName</td><td>$status</td></tr>";
}
my $privateChannels = "";
@ -146,6 +181,10 @@ sub handlePath {
$query = $aConnection->prepare(qq(select id, privileges from users where name=?;));
$query->execute($frontend_session::sessions{$aRequest->{"cookies"}{"session"}}{"username"});
my @row = $query->fetchrow_array();
if(scalar(@row)==0) {
frontend::redirect($aClient, "/");
return 1;
}
my $id = $row[0];
my $privileges = $row[1];
if($privileges>0) {
@ -158,13 +197,15 @@ sub handlePath {
}
while(@row = $query->fetchrow_array()) {
my $channelID = $row[0];
my $channelQuery = $aConnection->prepare(qq(select channels.name, servers.name from channels inner join servers on channels.server_id=servers.id where channels.id=$channelID;));
my $channelQuery = $aConnection->prepare(qq(select channels.name, channels.enabled, servers.name, servers.enabled from channels inner join servers on channels.server_id=servers.id where channels.id=$channelID;));
$channelQuery->execute();
@row = $channelQuery->fetchrow_array();
my $channelName = $row[0];
$channelName =~ s/%23/#/;
my $serverName = $row[1];
$privateChannels.="<tr><td><a href=\"view_logs?channel=$channelID\">$channelName</a></td><td>$serverName</td></tr>";
my $channelEnabled = $row[1] && $row[3];
my $serverName = $row[2];
my $status = $channelEnabled?"<span style=\"color:green\">Enabled</span>":"<span style=\"color:gray\">Disabled</span>";
$privateChannels.="<tr><td><a href=\"view_logs?channel=$channelID\">$channelName</a></td><td>$serverName</td><td>$status</td></tr>";
}
}
@ -216,7 +257,7 @@ sub handlePath {
my $response = frontend::getBaseResponse(301, "OK");
$response.="Location: /\r\n";
$response.="Content-Length: 0\r\n";
$response.="Set-Cookie: session=$token\r\n\r\n";
$response.="Set-Cookie: session=$token;expires=".localtime(time()+7*24*3600)."\r\n\r\n";
$aClient->send($response);
return 1;
}
@ -236,25 +277,19 @@ sub handlePath {
my $query = $aConnection->prepare(qq(select privileges from users where name=?;));
$query->execute($session->{"username"});
my @row = $query->fetchrow_array();
if(scalar(@row)==0) {
frontend::redirect($aClient, "/");
return 1;
}
my $privileges = $row[0];
my $manageChannelAccess = "";
my $addUser = "";
my $updateUser = "";
if($privileges>=1) { # moderator
$manageChannelAccess.="<h3>Manage channel access</h3>";
$manageChannelAccess.="<form action=\"manage_access_action\" method=\"POST\">";
$manageChannelAccess.="<select name=\"user\">";
$query = $aConnection->prepare(qq(select id, name from users;));
$query->execute();
while(@row = $query->fetchrow_array()) {
my $id = $row[0];
my $name = $row[1];
if($name eq $session->{"username"}) {
next;
}
$manageChannelAccess.="<option value=\"$id\">$name</option>";
}
$manageChannelAccess.="</select>";
$manageChannelAccess.=enumerateUsers($aConnection, $session)." ";
$manageChannelAccess.=enumerateChannels($aConnection)."<br />";
$manageChannelAccess.="<input name=\"grant\" type=\"submit\" value=\"Grant access\" /> ";
$manageChannelAccess.="<input name=\"revoke\" type=\"submit\" value=\"Revoke access\" />";
@ -268,9 +303,18 @@ sub handlePath {
$addUser.="<input name=\"operator\" type=\"checkbox\" />Operator<br />";
$addUser.="<input type=\"submit\" value=\"Add\" />";
$addUser.="</form>";
$updateUser.="<h3>Update user</h3>";
$updateUser.="<form action=\"update_user_action\" method=\"POST\">";
$updateUser.=enumerateUsers($aConnection, $session)."<br />";
$updateUser.="<input name=\"operator\" type=\"checkbox\" />Operator<br />";
$updateUser.="<input name=\"update\" type=\"submit\" value=\"Update\" /> ";
$updateUser.="<input name=\"delete\" type=\"submit\" value=\"Delete\" />";
$updateUser.="</form>";
}
my $addServer = "";
my $updateServer = "";
if($privileges==2) {
$addServer.="<h3>Add server</h3>";
$addServer.="<form action=\"add_server_action\" method=\"POST\">";
@ -279,6 +323,12 @@ sub handlePath {
$addServer.="<input name=\"port\" type=\"number\" placeholder=\"Server port (optional)\" /><br />";
$addServer.="<input type=\"submit\" value=\"Add\" />";
$addServer.="</form>";
$updateServer.="<h3>Update server</h3>";
$updateServer.="<form action=\"update_server_action\" method=\"POST\">";
$updateServer.=enumerateServers($aConnection)."<br />";
$updateServer.="<input name=\"enabled\" type=\"checkbox\" checked=\"true\" />Enabled<br />";
$updateServer.="<input type=\"submit\" value=\"Update\" />";
$updateServer.="</form>";
}
my $addChannel = "";
@ -304,11 +354,21 @@ sub handlePath {
$updateChannel.="<form action=\"update_channel_action\" method=\"POST\">";
$updateChannel.=enumerateChannels($aConnection)."<br />";
$updateChannel.="<input name=\"public\" type=\"checkbox\" />Public<br />";
$updateChannel.="<input name=\"enabled\" type=\"checkbox\" checked=\"true\" />Enabled<br />";
$updateChannel.="<input type=\"submit\" value=\"Update\" />";
$updateChannel.="</form>";
}
frontend::sendTemplate("templates/panel.html", $aClient, {"username"=>$session->{"username"}, "manageChannelAccess"=>$manageChannelAccess, "addUser"=>$addUser, "addServer"=>$addServer, "addChannel"=>$addChannel, "updateChannel"=>$updateChannel});
frontend::sendTemplate("templates/panel.html", $aClient, {
"username"=>$session->{"username"},
"manageChannelAccess"=>$manageChannelAccess,
"addUser"=>$addUser,
"updateUser"=>$updateUser,
"addServer"=>$addServer,
"updateServer"=>$updateServer,
"addChannel"=>$addChannel,
"updateChannel"=>$updateChannel
});
return 1;
}
when("/change_password_action") {
@ -355,6 +415,35 @@ sub handlePath {
return 1;
}
when("/delete_account_action") {
if(!defined($aRequest->{"cookies"}{"session"}) || !frontend_session::isValidSession($aRequest->{"cookies"}{"session"})) {
frontend::redirect($aClient, "/");
return 1;
}
my $session = $frontend_session::sessions{$aRequest->{"cookies"}{"session"}};
my %parameters = frontend::parsePathParameters($aRequest->{"content"});
if(!defined($parameters{"password"})) {
frontend::sendBadRequest($aClient, "Password parameter required");
return 1;
}
my $query = $aConnection->prepare(qq(select id, password from users where name=?;));
$query->execute($session->{"username"});
my @row = $query->fetchrow_array();
my $id = $row[0];
my $password = $row[1];
if($id==0) {
frontend::sendBadRequest($aClient, "Cannot delete user with ID 0 (admin)");
return 1;
}
if($password ne Digest::SHA::sha256_hex($parameters{"password"})) {
frontend::sendBadRequest($aClient, "Wrong password");
return 1;
}
frontend::deleteUser($id, $aConnection);
frontend_session::deleteSession($aRequest->{"cookies"}{"session"});
frontend::redirect($aClient, "/account_deleted.html");
return 1;
}
when("/manage_access_action") {
if(!verifyRequestPrivileges($aRequest, $aClient, 1, $aConnection)) {
return 1;
@ -461,6 +550,39 @@ sub handlePath {
frontend::redirect($aClient, "/user_added.html");
return 1;
}
when("/update_user_action") {
if(!verifyRequestPrivileges($aRequest, $aClient, 1, $aConnection)) {
return 1;
}
my %parameters = frontend::parsePathParameters($aRequest->{"content"});
if(!defined($parameters{"user"}) || length($parameters{"user"})==0) {
frontend::sendBadRequest($aClient, "User required");
return 1;
}
my $query = $aConnection->prepare(qq(select privileges from users where id=?;));
$query->execute($parameters{"user"});
my @row = $query->fetchrow_array();
if(scalar(@row)==0) {
frontend::sendBadRequest($aClient, "User with ID $parameters{'user'} doesn't exist");
return 1;
}
if($row[0]>1 && !verifyRequestPrivileges($aRequest, $aClient, 2, $aConnection)) {
return 1;
}
if(defined($parameters{"update"})) {
$query = $aConnection->prepare(qq(update users set privileges=? where id=?;));
$query->execute(defined($parameters{"operator"})?1:0, $parameters{"user"});
}
elsif(defined($parameters{"delete"})) {
frontend::deleteUser($parameters{"user"}, $aConnection);
}
else {
frontend::sendBadRequest($aClient, "Action (update or delete) required");
return 1;
}
frontend::redirect($aClient, "/user_updated.html");
return 1;
}
when("/add_server_action") {
if(!verifyRequestPrivileges($aRequest, $aClient, 2, $aConnection)) {
return 1;
@ -495,12 +617,53 @@ sub handlePath {
$lastID = $row[0]+1;
}
$query = $aConnection->prepare(qq(insert into servers values($lastID, ?, ?, ?);));
$query = $aConnection->prepare(qq(insert into servers values($lastID, ?, ?, ?, 1);));
$query->execute($parameters{"name"}, $parameters{"address"}, $port);
frontend::redirect($aClient, "/server_added.html");
logger::createLogger($parameters{"name"}, $parameters{"address"}, $port, ());
return 1;
}
when("/update_server_action") {
if(!verifyRequestPrivileges($aRequest, $aClient, 2, $aConnection)) {
return 1;
}
my %parameters = frontend::parsePathParameters($aRequest->{"content"});
if(!defined($parameters{"server"}) || length($parameters{"server"})==0) {
frontend::sendBadRequest($aClient, "Server required");
return 1;
}
my $query = $aConnection->prepare(qq(select name, host, port, enabled from servers where id=?;));
$query->execute($parameters{"server"});
my @row = $query->fetchrow_array();
if(scalar(@row)==0) {
frontend::sendBadRequest($aClient, "Server with ID $parameters{'server'} doesn't exist");
return 1;
}
my $server = $row[0];
my $serverEnabled = $row[3];
if(defined($parameters{"enabled"}) && !$serverEnabled) {
my $host = $row[1];
my $port = $row[2];
$query = $aConnection->prepare(qq(select name, enabled from channels where server_id=?;));
$query->execute($parameters{"server"});
my @channels;
while(@row = $query->fetchrow_array()) {
if(!$row[1]) {
next;
}
push(@channels, $row[0]);
}
logger::createLogger($server, $host, $port, \@channels);
}
elsif($serverEnabled) {
my $actionQueue = logger::getActionQueueByServerName($server);
push(@$actionQueue, "QUIT");
}
$query = $aConnection->prepare(qq(update servers set enabled=? where id=?;));
$query->execute(defined($parameters{"enabled"})?1:0, $parameters{"server"});
frontend::redirect($aClient, "/server_updated.html");
return 1;
}
when("/add_channel_action") {
if(!verifyRequestPrivileges($aRequest, $aClient, 2, $aConnection)) {
return 1;
@ -541,7 +704,7 @@ sub handlePath {
$lastID = $row[0]+1;
}
$query = $aConnection->prepare(qq(insert into channels values($lastID, ?, ?, ?);));
$query = $aConnection->prepare(qq(insert into channels values($lastID, ?, ?, ?, 1);));
$query->execute($parameters{"server"}, $parameters{"channel"}, defined($parameters{"public"})?1:0);
my $actionQueue = logger::getActionQueueByServerName($serverName);
push(@$actionQueue, "JOIN", $parameters{"channel"});
@ -558,15 +721,27 @@ sub handlePath {
frontend::sendBadRequest($aClient, "Channel required");
return 1;
}
my $query = $aConnection->prepare(qq(select id from channels where id=?;));
my $query = $aConnection->prepare(qq(select name, server_id, enabled from channels where id=?;));
$query->execute($parameters{"channel"});
my @row = $query->fetchrow_array();
if(scalar(@row)==0) {
frontend::sendBadRequest($aClient, "Channel with ID $parameters{'channel'} doesn't exist");
return 1;
}
$query = $aConnection->prepare(qq(update channels set public=? where id=?;));
$query->execute(defined($parameters{"public"})?1:0, $parameters{"channel"});
my $channel = $row[0];
my $channelEnabled = $row[2];
$query = $aConnection->prepare(qq(select name from servers where id=?;));
$query->execute($row[1]);
@row = $query->fetchrow_array();
my $actionQueue = logger::getActionQueueByServerName($row[0]);
if(defined($parameters{"enabled"}) && !$channelEnabled) {
push(@$actionQueue, "JOIN", $channel);
}
elsif($channelEnabled) {
push(@$actionQueue, "PART", $channel);
}
$query = $aConnection->prepare(qq(update channels set public=?, enabled=? where id=?;));
$query->execute(defined($parameters{"public"})?1:0, defined($parameters{"enabled"})?1:0, $parameters{"channel"});
frontend::redirect($aClient, "/channel_updated.html");
return 1;
}

View File

@ -22,9 +22,12 @@ use strict;
use warnings;
our %sessions;
my %sessionAccess;
sub newSessionToken {
return Digest::SHA::sha256_hex(sprintf("%x", rand(0xFFFFFFFF)));
my $session = Digest::SHA::sha256_hex(sprintf("%x", rand(0xFFFFFFFF)));
$sessionAccess{$session} = time();
return $session;
}
sub deleteSession {
@ -32,13 +35,23 @@ sub deleteSession {
if(isValidSession($aSession)) {
delete $sessions{$aSession};
delete $sessionAccess{$aSession};
}
}
sub isValidSession {
my $aSession = $_[0];
return defined($sessions{$aSession});
foreach my $key (keys(%sessionAccess)) {
if(time()-$sessionAccess{$key}>7*24*3600) {
deleteSession($key);
}
}
if(defined($sessions{$aSession})) {
$sessionAccess{$aSession} = time();
return 1;
}
return 0;
}
1;

100
logger.pm
View File

@ -308,6 +308,36 @@ sub handlePart {
$aLogFiles->{$aCommand->[1]}{"file"}->flush();
}
sub handleNick {
my $aCommand = $_[0];
my $aServerName = $_[1];
my $aLogFiles = $_[2];
my $aCommandLength = scalar(@$aCommand);
if($aCommandLength!=3) {
print("[error] Encountered invalid NICK command (3 arguments expected, $aCommandLength provided)\n");
return;
}
my $username = getUsernameFromHost($aCommand->[2]);
foreach my $channel (keys(%$aLogFiles)) {
my $found = 0;
my $i = 0;
foreach $i (0..scalar(@{$aLogFiles->{$channel}{"names"}})-1) {
my $name = \$aLogFiles->{$channel}{"names"}[$i];
if($$name eq $username) {
$found = 1;
$$name = $aCommand->[1];
last;
}
}
if(!$found || !prepareLogFile($aLogFiles, $aServerName, $channel)) {
next;
}
$aLogFiles->{$channel}{"file"}->print(sprintf("(%s) %s is now known as %s\n", localtime->strftime("%H:%M:%S"), $username, $aCommand->[1]));
$aLogFiles->{$channel}{"file"}->flush();
}
}
sub joinChannel {
my $aStream = $_[0];
my $aChannel = $_[1];
@ -324,6 +354,19 @@ sub joinChannels {
}
}
sub partChannel {
my $aStream = $_[0];
my $aChannel = $_[1];
$aStream->send(sprintf("PART %s\r\n", $aChannel));
}
sub quitFromServer {
my $aStream = $_[0];
$aStream->send("QUIT\r\n");
}
sub handleNames {
my $aCommand = $_[0];
my $aChannels = $_[1];
@ -341,6 +384,30 @@ sub handleNames {
push(@{$aLogFiles->{$aCommand->[3]}{"names"}}, @names);
}
sub handleTopic {
my $aCommand = $_[0];
my $aServerName = $_[1];
my $aLogFiles = $_[2];
my $aChangedByUser = $_[3];
my $aCommandLength = scalar(@$aCommand);
if($aCommandLength!=5) {
print("[error] Encountered invalid TOPIC command (5 arguments expected, $aCommandLength provided)\n");
return;
}
if(!prepareLogFile($aLogFiles, $aServerName, $aCommand->[2])) {
return;
}
if($aChangedByUser) {
my $username = getUsernameFromHost($aCommand->[4]);
$aLogFiles->{$aCommand->[2]}{"file"}->print(sprintf("(%s) %s changed topic for channel %s to: %s\n", localtime->strftime("%H:%M:%S"), $username, $aCommand->[2], $aCommand->[3]));
}
else {
$aLogFiles->{$aCommand->[2]}{"file"}->print(sprintf("(%s) Topic for channel %s: %s\n", localtime->strftime("%H:%M:%S"), $aCommand->[2], $aCommand->[3]));
}
$aLogFiles->{$aCommand->[2]}{"file"}->flush();
}
our @connections :shared;
our $running :shared = 1;
@ -352,17 +419,21 @@ sub connectionWorker {
my $buffer = "";
my @actionQueue :shared;
my $running = 1;
my @connection :shared = ($aServerName, \@actionQueue);
push(@connections, \@connection);
my %logFiles;
while($running) {
my $stream = connectToServer($aHost, $aPort, $aServerName);
my $streamSelect = IO::Select->new($stream);
while(!eof($stream)) {
while(!eof($stream) && $running) {
if(scalar(@actionQueue)>0) {
given($actionQueue[0]) {
when("JOIN") {
joinChannel($stream, $actionQueue[1]);
when("JOIN") { joinChannel($stream, $actionQueue[1]); }
when("PART") { partChannel($stream, $actionQueue[1]); }
when("QUIT") {
quitFromServer($stream);
$running = 0;
}
}
@actionQueue = ();
@ -388,8 +459,11 @@ sub connectionWorker {
when("JOIN") { handleJoin(\@command, $aServerName, \%logFiles); }
when("QUIT") { handleQuit(\@command, $aServerName, \%logFiles); }
when("PART") { handlePart(\@command, $aServerName, \%logFiles); }
when("NICK") { handleNick(\@command, $aServerName, \%logFiles); }
when("TOPIC") { handleTopic(\@command, $aServerName, \%logFiles, 1); }
when("376") { joinChannels($stream, $aChannels); } # end of MOTD
when("353") { handleNames(\@command, $aChannels, \%logFiles); } # NAMES reply
when("332") { handleTopic(\@command, $aServerName, \%logFiles, 0); } # TOPIC reply
}
($line, $remaining) = readLineFromBuffer($buffer);
$buffer = $remaining;
@ -397,6 +471,12 @@ sub connectionWorker {
}
close($stream);
}
foreach my $i (0..scalar(@connections)-1) {
if($connections[$i][0] eq $aServerName) {
$connections[$i][0] = "";
last;
}
}
}
sub createLogger {
@ -426,12 +506,20 @@ while(my @row = $query->fetchrow_array()) {
my $name = $row[1];
my $host = $row[2];
my $port = $row[3];
my $enabled = $row[4];
$query = $db->prepare(qq(select name from channels where server_id=$id;));
$query->execute();
if(!$enabled) {
next;
}
my $channelQuery = $db->prepare(qq(select name, enabled from channels where server_id=$id;));
$channelQuery->execute();
my @channels;
while(my @channelsRow = $query->fetchrow_array()) {
while(my @channelsRow = $channelQuery->fetchrow_array()) {
my $name = $channelsRow[0];
my $enabled = $channelsRow[1];
if(!$enabled) {
next;
}
push(@channels, $name);
}
createLogger($name, $host, $port, \@channels);

View File

@ -0,0 +1,10 @@
<!DOCTYPE HTML>
<html>
<head>
<title>Account deleted</title>
</head>
<body>
<p>Your account successfully deleted</p>
<a href="/">Return to index</a>
</body>
</html>

View File

@ -0,0 +1,10 @@
<!DOCTYPE HTML>
<html>
<head>
<title>Server updated</title>
</head>
<body>
<p>Server successfully updated</p>
<a href="/panel">Return to user panel</a>
</body>
</html>

10
static/user_updated.html Normal file
View File

@ -0,0 +1,10 @@
<!DOCTYPE HTML>
<html>
<head>
<title>User updated</title>
</head>
<body>
<p>User successfully updated</p>
<a href="/panel">Return to user panel</a>
</body>
</html>

View File

@ -7,7 +7,7 @@
{{userbar}}
<h2>Channel list</h2>
<table border>
<tr><th>Channel</th><th>Network</th></tr>
<tr><th>Channel</th><th>Network</th><th>Status</th></tr>
{{publicChannels}}
{{privateChannels}}
</table>

View File

@ -12,9 +12,16 @@
<input name="newPassword" type="password" placeholder="New password"><br />
<input type="submit" value="Change" />
</form>
<h3>Delete this account</h3>
<form action="delete_account_action" method="POST">
<input name="password" type="password" placeholder="Password" /><br />
<input type="submit" value="Delete (this operation cannot be reverted!)" />
</form>
{{manageChannelAccess}}
{{addUser}}
{{updateUser}}
{{addServer}}
{{updateServer}}
{{addChannel}}
{{updateChannel}}
</body>