made admin interface work with mysql
This commit is contained in:
commit
1e6b119ebd
13 changed files with 413 additions and 55 deletions
|
@ -4,7 +4,8 @@ USE CalendarWebapp;
|
|||
CREATE TABLE users (
|
||||
id MEDIUMINT UNSIGNED NOT NULL AUTO_INCREMENT,
|
||||
username CHAR(30) NOT NULL UNIQUE,
|
||||
password CHAR(60) NOT NULL,
|
||||
passwordHash CHAR(60) NOT NULL,
|
||||
privilege TINYINT UNSIGNED NOT NULL,
|
||||
PRIMARY KEY (id)
|
||||
);
|
||||
|
||||
|
@ -19,5 +20,5 @@ CREATE TABLE events (
|
|||
PRIMARY KEY (id)
|
||||
);
|
||||
|
||||
INSERT INTO users (username, password) VALUES ('foo',
|
||||
'$2a$10$9LBqOZV99ARiE4Nx.2b7GeYfqk2.0A32PWGu2cRGyW2hRJ0xeDfnO');
|
||||
INSERT INTO users (username, passwordHash, privilege) VALUES ('foo',
|
||||
'$2a$10$9LBqOZV99ARiE4Nx.2b7GeYfqk2.0A32PWGu2cRGyW2hRJ0xeDfnO', 2);
|
||||
|
|
|
@ -1,12 +1,23 @@
|
|||
module calendarwebapp.authenticator;
|
||||
|
||||
import calendarwebapp.passhash : PasswordHasher;
|
||||
|
||||
import poodinis;
|
||||
|
||||
import std.conv : to;
|
||||
import std.range : InputRange;
|
||||
import std.typecons : nullable, Nullable;
|
||||
|
||||
import vibe.data.bson;
|
||||
|
||||
import vibe.db.mongo.collection : MongoCollection;
|
||||
|
||||
interface Authenticator
|
||||
{
|
||||
bool checkUser(string username, string password) @safe;
|
||||
Nullable!AuthInfo checkUser(string username, string password) @safe;
|
||||
void addUser(AuthInfo authInfo);
|
||||
InputRange!AuthInfo getAllUsers();
|
||||
void removeUser(string id);
|
||||
}
|
||||
|
||||
class MongoDBAuthenticator(Collection = MongoCollection) : Authenticator
|
||||
|
@ -14,20 +25,64 @@ class MongoDBAuthenticator(Collection = MongoCollection) : Authenticator
|
|||
private:
|
||||
@Value("users")
|
||||
Collection users;
|
||||
@Autowire PasswordHasher passwordHasher;
|
||||
|
||||
public:
|
||||
bool checkUser(string username, string password) @safe
|
||||
Nullable!AuthInfo checkUser(string username, string password) @safe
|
||||
{
|
||||
import botan.passhash.bcrypt : checkBcrypt;
|
||||
import vibe.data.bson : Bson;
|
||||
|
||||
auto result = users.findOne(["username" : username]);
|
||||
/* checkBcrypt should be called using vibe.core.concurrency.async to
|
||||
/* checkHash should be called using vibe.core.concurrency.async to
|
||||
avoid blocking, but https://github.com/vibe-d/vibe.d/issues/1521 is
|
||||
blocking this */
|
||||
return (result != Bson(null)) && (() @trusted => checkBcrypt(password,
|
||||
result["password"].get!string))();
|
||||
if (result != Bson(null))
|
||||
{
|
||||
auto authInfo = result.deserializeBson!AuthInfo;
|
||||
if (passwordHasher.checkHash(password, authInfo.passwordHash))
|
||||
{
|
||||
return authInfo.nullable;
|
||||
}
|
||||
}
|
||||
return Nullable!AuthInfo.init;
|
||||
}
|
||||
|
||||
void addUser(AuthInfo authInfo) @safe
|
||||
{
|
||||
import std.conv : ConvException;
|
||||
|
||||
try
|
||||
{
|
||||
if (!BsonObjectID.fromString(authInfo.id).valid)
|
||||
throw new ConvException("invalid BsonObjectID.");
|
||||
}
|
||||
catch (ConvException)
|
||||
{
|
||||
authInfo.id = BsonObjectID.generate.to!string;
|
||||
}
|
||||
|
||||
users.insert(authInfo.serializeToBson);
|
||||
}
|
||||
|
||||
InputRange!AuthInfo getAllUsers() @safe
|
||||
{
|
||||
import std.algorithm : map;
|
||||
import std.range : inputRangeObject;
|
||||
|
||||
return users.find().map!(deserializeBson!AuthInfo).inputRangeObject;
|
||||
}
|
||||
|
||||
void removeUser(string id) @safe
|
||||
{
|
||||
users.remove(["_id" : id]);
|
||||
}
|
||||
}
|
||||
|
||||
enum Privilege
|
||||
{
|
||||
None,
|
||||
User,
|
||||
Admin
|
||||
}
|
||||
|
||||
class MySQLAuthenticator : Authenticator
|
||||
|
@ -36,27 +91,108 @@ private:
|
|||
import mysql;
|
||||
|
||||
@Autowire MySQLPool pool;
|
||||
@Autowire PasswordHasher passwordHasher;
|
||||
|
||||
public:
|
||||
bool checkUser(string username, string password) @trusted
|
||||
Nullable!AuthInfo checkUser(string username, string password) @trusted
|
||||
{
|
||||
import botan.passhash.bcrypt : checkBcrypt;
|
||||
|
||||
auto cn = pool.lockConnection();
|
||||
scope (exit)
|
||||
cn.close();
|
||||
auto prepared = cn.prepare("SELECT password FROM users WHERE username = ?");
|
||||
auto prepared = cn.prepare(
|
||||
"SELECT id, username, passwordHash, privilege FROM users WHERE username = ?");
|
||||
prepared.setArg(0, username);
|
||||
auto result = prepared.query();
|
||||
|
||||
/* checkBcrypt should be called using vibe.core.concurrency.async to
|
||||
/* checkHash should be called using vibe.core.concurrency.async to
|
||||
avoid blocking, but https://github.com/vibe-d/vibe.d/issues/1521 is
|
||||
blocking this */
|
||||
return !result.empty && (() @trusted => checkBcrypt(password, result.front[0].get!string))();
|
||||
if (!result.empty)
|
||||
{
|
||||
auto authInfo = toAuthInfo(result.front);
|
||||
if (passwordHasher.checkHash(password, authInfo.passwordHash))
|
||||
{
|
||||
return authInfo.nullable;
|
||||
}
|
||||
}
|
||||
return Nullable!AuthInfo.init;
|
||||
}
|
||||
|
||||
void addUser(AuthInfo authInfo)
|
||||
{
|
||||
auto cn = pool.lockConnection();
|
||||
scope (exit)
|
||||
cn.close;
|
||||
auto prepared = cn.prepare(
|
||||
"INSERT INTO users (username, passwordHash, privilege) VALUES(?, ?, ?)");
|
||||
prepared.setArgs(authInfo.username, authInfo.passwordHash, authInfo.privilege.to!uint);
|
||||
prepared.exec();
|
||||
}
|
||||
|
||||
InputRange!AuthInfo getAllUsers()
|
||||
{
|
||||
import std.algorithm : map;
|
||||
import std.range : inputRangeObject;
|
||||
|
||||
auto cn = pool.lockConnection();
|
||||
scope (exit)
|
||||
cn.close;
|
||||
auto prepared = cn.prepare("SELECT id, username, passwordHash, privilege FROM users");
|
||||
return prepared.querySet.map!(r => toAuthInfo(r)).inputRangeObject;
|
||||
}
|
||||
|
||||
void removeUser(string id)
|
||||
{
|
||||
auto cn = pool.lockConnection();
|
||||
scope (exit)
|
||||
cn.close;
|
||||
auto prepared = cn.prepare("DELETE FROM users WHERE id = ?");
|
||||
prepared.setArg(0, id.to!uint);
|
||||
prepared.exec();
|
||||
}
|
||||
|
||||
private:
|
||||
|
||||
AuthInfo toAuthInfo(Row r)
|
||||
{
|
||||
import std.conv : to;
|
||||
|
||||
AuthInfo authInfo;
|
||||
authInfo.id = r[0].get!uint.to!string;
|
||||
authInfo.username = r[1].get!string;
|
||||
authInfo.passwordHash = r[2].get!string;
|
||||
authInfo.privilege = r[3].get!uint.to!Privilege;
|
||||
return authInfo;
|
||||
}
|
||||
}
|
||||
|
||||
struct AuthInfo
|
||||
{
|
||||
string userName;
|
||||
import vibe.data.serialization : name;
|
||||
|
||||
@name("_id") string id;
|
||||
string username;
|
||||
string passwordHash;
|
||||
Privilege privilege;
|
||||
|
||||
mixin(generateAuthMethods);
|
||||
|
||||
private:
|
||||
static string generateAuthMethods() pure @safe
|
||||
{
|
||||
import std.conv : to;
|
||||
import std.format : format;
|
||||
import std.traits : EnumMembers;
|
||||
|
||||
string ret;
|
||||
foreach (member; EnumMembers!Privilege)
|
||||
{
|
||||
ret ~= q{
|
||||
bool is%s() const pure @safe nothrow
|
||||
{
|
||||
return privilege == Privilege.%s;
|
||||
}
|
||||
}.format(member.to!string, member.to!string);
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,7 +1,8 @@
|
|||
module calendarwebapp.calendarwebapp;
|
||||
|
||||
import calendarwebapp.authenticator : Authenticator, AuthInfo;
|
||||
import calendarwebapp.authenticator;
|
||||
import calendarwebapp.event;
|
||||
import calendarwebapp.passhash : PasswordHasher;
|
||||
|
||||
import core.time : days;
|
||||
|
||||
|
@ -23,46 +24,49 @@ import vibe.web.web : errorDisplay, noRoute, redirect, render, SessionVar,
|
|||
{
|
||||
@noRoute AuthInfo authenticate(scope HTTPServerRequest req, scope HTTPServerResponse) @safe
|
||||
{
|
||||
if (!req.session || !req.session.isKeySet("auth"))
|
||||
{
|
||||
if (authInfo.value.isNone)
|
||||
redirect("/login");
|
||||
return AuthInfo.init;
|
||||
}
|
||||
return req.session.get!AuthInfo("auth");
|
||||
|
||||
return authInfo.value;
|
||||
}
|
||||
|
||||
public:
|
||||
@anyAuth void index()
|
||||
@auth(Role.user | Role.admin) void index()
|
||||
{
|
||||
auto events = eventStore.getAllEvents();
|
||||
render!("showevents.dt", events);
|
||||
auto authInfo = this.authInfo.value;
|
||||
render!("showevents.dt", events, authInfo);
|
||||
}
|
||||
|
||||
@noAuth void getLogin(string _error = null)
|
||||
{
|
||||
render!("login.dt", _error);
|
||||
auto authInfo = this.authInfo.value;
|
||||
render!("login.dt", _error, authInfo);
|
||||
}
|
||||
|
||||
@noAuth @errorDisplay!getLogin void postLogin(string username, string password) @safe
|
||||
{
|
||||
enforce(authenticator.checkUser(username, password), "Benutzername oder Passwort ungültig");
|
||||
immutable AuthInfo authInfo = {username};
|
||||
auth = authInfo;
|
||||
auto authInfo = authenticator.checkUser(username, password);
|
||||
enforce(!authInfo.isNull, "Benutzername oder Passwort ungültig");
|
||||
this.authInfo = authInfo.get;
|
||||
redirect("/");
|
||||
}
|
||||
|
||||
@anyAuth void getLogout() @safe
|
||||
@auth(Role.user | Role.admin) void getLogout() @safe
|
||||
{
|
||||
terminateSession();
|
||||
redirect("/");
|
||||
}
|
||||
|
||||
@anyAuth void getCreate(ValidationErrorData _error = ValidationErrorData.init)
|
||||
@auth(Role.user | Role.admin) void getCreateevent(
|
||||
ValidationErrorData _error = ValidationErrorData.init)
|
||||
{
|
||||
render!("create.dt", _error);
|
||||
auto authInfo = this.authInfo.value;
|
||||
render!("createevent.dt", _error, authInfo);
|
||||
}
|
||||
|
||||
@anyAuth @errorDisplay!getCreate void postCreate(Date begin,
|
||||
|
||||
@auth(Role.user | Role.admin) @errorDisplay!getCreateevent void postCreateevent(Date begin,
|
||||
Nullable!Date end, string description, string name, EventType type, bool shout)
|
||||
{
|
||||
import std.array : replace, split;
|
||||
|
@ -77,12 +81,39 @@ public:
|
|||
redirect("/");
|
||||
}
|
||||
|
||||
@anyAuth void postRemove(string id)
|
||||
@auth(Role.user | Role.admin) void postRemoveevent(string id)
|
||||
{
|
||||
eventStore.removeEvent(id);
|
||||
redirect("/");
|
||||
}
|
||||
|
||||
@auth(Role.admin) void getUsers()
|
||||
{
|
||||
auto users = authenticator.getAllUsers;
|
||||
auto authInfo = this.authInfo.value;
|
||||
render!("showusers.dt", users, authInfo);
|
||||
}
|
||||
|
||||
@auth(Role.admin) void postRemoveuser(string id)
|
||||
{
|
||||
authenticator.removeUser(id);
|
||||
redirect("/users");
|
||||
}
|
||||
|
||||
@auth(Role.admin) void getCreateuser(ValidationErrorData _error = ValidationErrorData.init)
|
||||
{
|
||||
auto authInfo = this.authInfo.value;
|
||||
render!("createuser.dt", _error, authInfo);
|
||||
}
|
||||
|
||||
@auth(Role.admin) @errorDisplay!getCreateuser void postCreateuser(string username,
|
||||
string password, Privilege role)
|
||||
{
|
||||
authenticator.addUser(AuthInfo("", username,
|
||||
passwordHasher.generateHash(password), role));
|
||||
redirect("/users");
|
||||
}
|
||||
|
||||
private:
|
||||
struct ValidationErrorData
|
||||
{
|
||||
|
@ -90,8 +121,10 @@ private:
|
|||
string field;
|
||||
}
|
||||
|
||||
SessionVar!(AuthInfo, "auth") auth;
|
||||
SessionVar!(AuthInfo, "authInfo") authInfo = AuthInfo("",
|
||||
string.init, string.init, Privilege.None);
|
||||
|
||||
@Autowire EventStore eventStore;
|
||||
@Autowire Authenticator authenticator;
|
||||
@Autowire PasswordHasher passwordHasher;
|
||||
}
|
||||
|
|
|
@ -1,8 +1,13 @@
|
|||
module calendarwebapp.configuration;
|
||||
|
||||
import calendarwebapp.authenticator : Authenticator, MongoDBAuthenticator, MySQLAuthenticator;
|
||||
import botan.rng.auto_rng : AutoSeededRNG;
|
||||
import botan.rng.rng : RandomNumberGenerator;
|
||||
|
||||
import calendarwebapp.authenticator : Authenticator, MongoDBAuthenticator,
|
||||
MySQLAuthenticator;
|
||||
import calendarwebapp.calendarwebapp : CalendarWebapp;
|
||||
import calendarwebapp.event : EventStore, MongoDBEventStore, MySQLEventStore;
|
||||
import calendarwebapp.passhash : BcryptPasswordHasher, PasswordHasher;
|
||||
|
||||
import mysql : MySQLPool;
|
||||
|
||||
|
@ -18,11 +23,14 @@ public:
|
|||
override void registerDependencies(shared(DependencyContainer) container)
|
||||
{
|
||||
auto mongoClient = connectMongoDB("localhost");
|
||||
auto pool = new MySQLPool("localhost", "username", "password", "CalendarWebapp");
|
||||
auto pool = new MySQLPool("localhost", "root", "Ilemm3Kzj", "CalendarWebapp");
|
||||
container.register!MySQLPool.existingInstance(pool);
|
||||
container.register!MongoClient.existingInstance(mongoClient);
|
||||
container.register!(EventStore, MySQLEventStore);
|
||||
container.register!(Authenticator, MySQLAuthenticator);
|
||||
|
||||
container.register!(PasswordHasher, BcryptPasswordHasher);
|
||||
container.register!(RandomNumberGenerator, AutoSeededRNG);
|
||||
container.register!CalendarWebapp;
|
||||
container.register!(ValueInjector!string, StringInjector);
|
||||
container.register!(ValueInjector!MongoCollection, MongoCollectionInjector);
|
||||
|
|
|
@ -129,8 +129,6 @@ private:
|
|||
|
||||
Event toEvent(Row r)
|
||||
{
|
||||
import std.conv : to;
|
||||
|
||||
Event event;
|
||||
event.id = r[0].get!uint.to!string;
|
||||
event.begin = r[1].get!Date;
|
||||
|
|
42
source/calendarwebapp/passhash.d
Normal file
42
source/calendarwebapp/passhash.d
Normal file
|
@ -0,0 +1,42 @@
|
|||
module calendarwebapp.passhash;
|
||||
|
||||
import poodinis;
|
||||
|
||||
interface PasswordHasher
|
||||
{
|
||||
string generateHash(in string password) @safe;
|
||||
bool checkHash(in string password, in string hash) const @safe;
|
||||
}
|
||||
|
||||
class BcryptPasswordHasher : PasswordHasher
|
||||
{
|
||||
import botan.passhash.bcrypt : checkBcrypt, generateBcrypt;
|
||||
import botan.rng.rng : RandomNumberGenerator;
|
||||
|
||||
string generateHash(in string password) @safe
|
||||
{
|
||||
return (() @trusted => generateBcrypt(password, rng, cost))();
|
||||
}
|
||||
|
||||
bool checkHash(in string password, in string hash) const @safe
|
||||
{
|
||||
return (()@trusted => checkBcrypt(password, hash))();
|
||||
}
|
||||
|
||||
private:
|
||||
@Autowire RandomNumberGenerator rng;
|
||||
enum cost = 10;
|
||||
}
|
||||
|
||||
class StubPasswordHasher : PasswordHasher
|
||||
{
|
||||
string generateHash(in string password) const @safe pure nothrow
|
||||
{
|
||||
return password;
|
||||
}
|
||||
|
||||
bool checkHash(in string password, in string hash) const @safe pure nothrow
|
||||
{
|
||||
return password == hash;
|
||||
}
|
||||
}
|
|
@ -1,6 +1,7 @@
|
|||
module test.calendarwebapp.testauthenticator;
|
||||
|
||||
import calendarwebapp.authenticator;
|
||||
import calendarwebapp.passhash : PasswordHasher, StubPasswordHasher;
|
||||
|
||||
import poodinis;
|
||||
|
||||
|
@ -11,7 +12,10 @@ import vibe.data.bson : Bson, BsonObjectID;
|
|||
|
||||
interface Collection
|
||||
{
|
||||
Bson[] find() @safe;
|
||||
Bson findOne(string[string] query) @safe;
|
||||
void insert(Bson document) @safe;
|
||||
void remove(string[string] selector) @safe;
|
||||
}
|
||||
|
||||
class CollectionInjector : ValueInjector!Collection
|
||||
|
@ -31,7 +35,7 @@ public:
|
|||
}
|
||||
}
|
||||
|
||||
@("Test MongoDBAuthenticator")
|
||||
@("MongoDBAuthenticator.checkUser")
|
||||
@system unittest
|
||||
{
|
||||
auto collection = mock!Collection;
|
||||
|
@ -40,14 +44,63 @@ public:
|
|||
container.resolve!CollectionInjector.add("users", collection);
|
||||
container.register!(Authenticator, MongoDBAuthenticator!(Collection))(
|
||||
RegistrationOption.doNotAddConcreteTypeRegistration);
|
||||
container.register!(PasswordHasher, StubPasswordHasher);
|
||||
|
||||
auto userBson = Bson(["_id" : Bson(BsonObjectID.fromString("5988ef4ae6c19089a1a53b79")), "username" : Bson("foo"),
|
||||
"password" : Bson("$2a$10$9LBqOZV99ARiE4Nx.2b7GeYfqk2.0A32PWGu2cRGyW2hRJ0xeDfnO")]);
|
||||
auto userBson = Bson(["_id" : Bson("5988ef4ae6c19089a1a53b79"), "username"
|
||||
: Bson("foo"), "passwordHash" : Bson("bar"), "privilege" : Bson(1)]);
|
||||
|
||||
collection.returnValue!"findOne"(Bson(null), userBson, userBson);
|
||||
|
||||
auto authenticator = container.resolve!(Authenticator);
|
||||
authenticator.checkUser("", "").shouldBeFalse;
|
||||
authenticator.checkUser("foo", "bar").shouldBeTrue;
|
||||
authenticator.checkUser("foo", "baz").shouldBeFalse;
|
||||
authenticator.checkUser("", "").isNull.shouldBeTrue;
|
||||
authenticator.checkUser("foo", "bar").isNull.shouldBeFalse;
|
||||
authenticator.checkUser("foo", "baz").isNull.shouldBeTrue;
|
||||
}
|
||||
|
||||
@("AuthInfo.isUser success")
|
||||
@safe unittest
|
||||
{
|
||||
AuthInfo auth;
|
||||
auth.privilege = Privilege.User;
|
||||
auth.isUser.shouldBeTrue;
|
||||
}
|
||||
|
||||
@("AuthInfo.isUser failure")
|
||||
@safe unittest
|
||||
{
|
||||
AuthInfo auth;
|
||||
auth.privilege = Privilege.None;
|
||||
auth.isUser.shouldBeFalse;
|
||||
}
|
||||
|
||||
@("AuthInfo.isAdmin success")
|
||||
@safe unittest
|
||||
{
|
||||
AuthInfo auth;
|
||||
auth.privilege = Privilege.Admin;
|
||||
auth.isAdmin.shouldBeTrue;
|
||||
}
|
||||
|
||||
@("AuthInfo.isAdmin failure")
|
||||
@safe unittest
|
||||
{
|
||||
AuthInfo auth;
|
||||
auth.privilege = Privilege.None;
|
||||
auth.isAdmin.shouldBeFalse;
|
||||
}
|
||||
|
||||
@("AuthInfo.isNone success")
|
||||
@safe unittest
|
||||
{
|
||||
AuthInfo auth;
|
||||
auth.privilege = Privilege.None;
|
||||
auth.isNone.shouldBeTrue;
|
||||
}
|
||||
|
||||
@("AuthInfo.isNone failure")
|
||||
@safe unittest
|
||||
{
|
||||
AuthInfo auth;
|
||||
auth.privilege = Privilege.User;
|
||||
auth.isNone.shouldBeFalse;
|
||||
}
|
||||
|
|
31
test/calendarwebapp/testpasshash.d
Normal file
31
test/calendarwebapp/testpasshash.d
Normal file
|
@ -0,0 +1,31 @@
|
|||
module test.calendarwebapp.testpasshash;
|
||||
|
||||
import calendarwebapp.passhash;
|
||||
|
||||
import poodinis;
|
||||
|
||||
import unit_threaded;
|
||||
|
||||
@("BcryptPasswordHasher")
|
||||
@Values("", "test", "langesKompliziertesPasswort")
|
||||
@system unittest
|
||||
{
|
||||
import botan.rng.rng : RandomNumberGenerator;
|
||||
import botan.rng.auto_rng : AutoSeededRNG;
|
||||
auto container = new shared DependencyContainer;
|
||||
container.register!(RandomNumberGenerator, AutoSeededRNG);
|
||||
container.register!(PasswordHasher, BcryptPasswordHasher);
|
||||
|
||||
auto hasher = container.resolve!PasswordHasher;
|
||||
immutable testPassword = getValue!string;
|
||||
hasher.checkHash(testPassword, hasher.generateHash(testPassword)).shouldBeTrue;
|
||||
}
|
||||
|
||||
@("StubPasswordHasher")
|
||||
@Values("", "test", "langesKompliziertesPasswort")
|
||||
@safe unittest
|
||||
{
|
||||
immutable hasher = new StubPasswordHasher;
|
||||
immutable testPassword = getValue!string;
|
||||
hasher.checkHash(testPassword, hasher.generateHash(testPassword)).shouldBeTrue;
|
||||
}
|
|
@ -3,7 +3,7 @@ block content
|
|||
- void showerror(string field = null)
|
||||
- if (_error.msg && _error.field == field)
|
||||
td.error= _error.msg
|
||||
form(action="/create", method="post")
|
||||
form(action="/createevent", method="post")
|
||||
fieldset(name="eventFields")
|
||||
table
|
||||
tbody#fieldTable
|
32
views/createuser.dt
Normal file
32
views/createuser.dt
Normal file
|
@ -0,0 +1,32 @@
|
|||
extends layout
|
||||
block content
|
||||
- void showerror(string field = null)
|
||||
- if (_error.msg && _error.field == field)
|
||||
td.error= _error.msg
|
||||
form(action="/createuser", method="post")
|
||||
fieldset(name="eventFields")
|
||||
table
|
||||
tbody#fieldTable
|
||||
tr
|
||||
td
|
||||
label(for="username") Benutzername
|
||||
td
|
||||
input#username(value="", name="username", type="text")
|
||||
- showerror("username");
|
||||
tr
|
||||
td
|
||||
label(for="password") Passwort
|
||||
td
|
||||
input#password(value="", name="password", type="password")
|
||||
tr
|
||||
td
|
||||
label(for="role") Rolle
|
||||
td
|
||||
select#type(name="role")
|
||||
option(value="User") Benutzer
|
||||
option(value="Admin") Administrator
|
||||
- showerror("role");
|
||||
tfoot
|
||||
tr
|
||||
td(colspan="2")
|
||||
input#submitButton(type="submit", value="Benutzer erstellen")
|
|
@ -1,8 +1,14 @@
|
|||
nav
|
||||
- if(!authInfo.isNone())
|
||||
nav
|
||||
ul
|
||||
li
|
||||
a(href='/') Home
|
||||
li
|
||||
a(href='/create') Ereignis erstellen
|
||||
a(href='/createevent') Ereignis erstellen
|
||||
- if(authInfo.isAdmin())
|
||||
li
|
||||
a(href='/users') Benutzerliste
|
||||
li
|
||||
a(href='/createuser') Benutzer erstellen
|
||||
li
|
||||
a(href='/logout') Ausloggen
|
|
@ -24,7 +24,7 @@ block content
|
|||
tr
|
||||
td shout
|
||||
td #{event.shout}
|
||||
form(action="/remove", method="post")
|
||||
form(action="/removeevent", method="post")
|
||||
input#id(value="#{event.id}", name="id", type="hidden")
|
||||
input#submitButton(type="submit", value="Entfernen")
|
||||
hr
|
||||
|
|
18
views/showusers.dt
Normal file
18
views/showusers.dt
Normal file
|
@ -0,0 +1,18 @@
|
|||
extends layout.dt
|
||||
block content
|
||||
h1 Users
|
||||
- foreach (user; users)
|
||||
table
|
||||
tr
|
||||
td id
|
||||
td #{user.id}
|
||||
tr
|
||||
td username
|
||||
td #{user.username}
|
||||
tr
|
||||
td privilege
|
||||
td #{user.privilege}
|
||||
form(action="/removeuser", method="post")
|
||||
input#id(value="#{user.id}", name="id", type="hidden")
|
||||
input#submitButton(type="submit", value="Entfernen")
|
||||
hr
|
Loading…
Reference in a new issue