Merge branch 'mysql' into 'master'

Mysql support

Closes #10

See merge request fsimphy/calendar-webapp!14
This commit is contained in:
Oliver Rümpelein 2017-11-20 21:26:20 +01:00
commit 612ff77911
9 changed files with 248 additions and 46 deletions

View file

@ -4,6 +4,7 @@
"Johannes Loher" "Johannes Loher"
], ],
"dependencies": { "dependencies": {
"mysql-native": "~>1.1.2",
"botan": "~>1.12.9", "botan": "~>1.12.9",
"vibe-d": "~>0.8.1", "vibe-d": "~>0.8.1",
"vibe-d:tls": "~>0.8.1", "vibe-d:tls": "~>0.8.1",

24
schema.sql Normal file
View file

@ -0,0 +1,24 @@
CREATE DATABASE CalendarWebapp;
USE CalendarWebapp;
CREATE TABLE users (
id MEDIUMINT UNSIGNED NOT NULL AUTO_INCREMENT,
username CHAR(30) NOT NULL UNIQUE,
passwordHash CHAR(60) NOT NULL,
privilege TINYINT UNSIGNED NOT NULL,
PRIMARY KEY (id)
);
CREATE TABLE events (
id MEDIUMINT UNSIGNED NOT NULL AUTO_INCREMENT,
begin DATE NOT NULL,
end DATE,
name CHAR(128) NOT NULL,
description TEXT NOT NULL,
type TINYINT UNSIGNED NOT NULL,
shout BOOLEAN NOT NULL,
PRIMARY KEY (id)
);
INSERT INTO users (username, passwordHash, privilege) VALUES ('foo',
'$2a$10$9LBqOZV99ARiE4Nx.2b7GeYfqk2.0A32PWGu2cRGyW2hRJ0xeDfnO', 2);

View file

@ -4,18 +4,20 @@ import calendarwebapp.passhash : PasswordHasher;
import poodinis; import poodinis;
import std.conv : to;
import std.range : InputRange; import std.range : InputRange;
import std.typecons : nullable, Nullable; import std.typecons : nullable, Nullable;
import vibe.data.bson; import vibe.data.bson;
import vibe.db.mongo.collection : MongoCollection; import vibe.db.mongo.collection : MongoCollection;
interface Authenticator interface Authenticator
{ {
Nullable!AuthInfo checkUser(string username, string password) @safe; Nullable!AuthInfo checkUser(string username, string password) @safe;
void addUser(AuthInfo authInfo) @safe; void addUser(AuthInfo authInfo);
InputRange!AuthInfo getAllUsers() @safe; InputRange!AuthInfo getAllUsers();
void removeUser(BsonObjectID id) @safe; void removeUser(string id);
} }
class MongoDBAuthenticator(Collection = MongoCollection) : Authenticator class MongoDBAuthenticator(Collection = MongoCollection) : Authenticator
@ -47,8 +49,17 @@ public:
void addUser(AuthInfo authInfo) @safe void addUser(AuthInfo authInfo) @safe
{ {
if (!authInfo.id.valid) import std.conv : ConvException;
authInfo.id = BsonObjectID.generate;
try
{
if (!BsonObjectID.fromString(authInfo.id).valid)
throw new ConvException("invalid BsonObjectID.");
}
catch (ConvException)
{
authInfo.id = BsonObjectID.generate.to!string;
}
users.insert(authInfo.serializeToBson); users.insert(authInfo.serializeToBson);
} }
@ -61,7 +72,7 @@ public:
return users.find().map!(deserializeBson!AuthInfo).inputRangeObject; return users.find().map!(deserializeBson!AuthInfo).inputRangeObject;
} }
void removeUser(BsonObjectID id) @safe void removeUser(string id) @safe
{ {
users.remove(["_id" : id]); users.remove(["_id" : id]);
} }
@ -74,11 +85,91 @@ enum Privilege
Admin Admin
} }
class MySQLAuthenticator : Authenticator
{
private:
import mysql;
@Autowire MySQLPool pool;
@Autowire PasswordHasher passwordHasher;
public:
Nullable!AuthInfo checkUser(string username, string password) @trusted
{
auto cn = pool.lockConnection();
scope (exit)
cn.close();
auto prepared = cn.prepare(
"SELECT id, username, passwordHash, privilege FROM users WHERE username = ?");
prepared.setArg(0, username);
auto result = prepared.query();
/* 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 */
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 struct AuthInfo
{ {
import vibe.data.serialization : name; import vibe.data.serialization : name;
@name("_id") BsonObjectID id; @name("_id") string id;
string username; string username;
string passwordHash; string passwordHash;
Privilege privilege; Privilege privilege;

View file

@ -65,23 +65,23 @@ public:
render!("createevent.dt", _error, authInfo); render!("createevent.dt", _error, authInfo);
} }
@auth(Role.user | Role.admin) @errorDisplay!getCreateevent void postCreateevent(Date begin, @auth(Role.user | Role.admin) @errorDisplay!getCreateevent void postCreateevent(Date begin,
Nullable!Date end, string description, string name, EventType type, bool shout) @safe Nullable!Date end, string description, string name, EventType type, bool shout)
{ {
import std.array : replace, split; import std.array : replace, split;
if (!end.isNull) if (!end.isNull)
enforce(end - begin >= 1.days, enforce(end - begin >= 1.days,
"Mehrtägige Ereignisse müssen mindestens einen Tag dauern"); "Mehrtägige Ereignisse müssen mindestens einen Tag dauern");
auto event = Event(BsonObjectID.generate, begin, end, name, auto event = Event("", begin, end, name, description.replace("\r", ""), type, shout);
description.replace("\r", ""), type, shout);
eventStore.addEvent(event); eventStore.addEvent(event);
redirect("/"); redirect("/");
} }
@auth(Role.user | Role.admin) void postRemoveevent(BsonObjectID id) @safe @auth(Role.user | Role.admin) void postRemoveevent(string id)
{ {
eventStore.removeEvent(id); eventStore.removeEvent(id);
redirect("/"); redirect("/");
@ -94,7 +94,7 @@ public:
render!("showusers.dt", users, authInfo); render!("showusers.dt", users, authInfo);
} }
@auth(Role.admin) void postRemoveuser(BsonObjectID id) @safe @auth(Role.admin) void postRemoveuser(string id)
{ {
authenticator.removeUser(id); authenticator.removeUser(id);
redirect("/users"); redirect("/users");
@ -107,9 +107,9 @@ public:
} }
@auth(Role.admin) @errorDisplay!getCreateuser void postCreateuser(string username, @auth(Role.admin) @errorDisplay!getCreateuser void postCreateuser(string username,
string password, Privilege role) @safe string password, Privilege role)
{ {
authenticator.addUser(AuthInfo(BsonObjectID.generate, username, authenticator.addUser(AuthInfo("", username,
passwordHasher.generateHash(password), role)); passwordHasher.generateHash(password), role));
redirect("/users"); redirect("/users");
} }
@ -121,7 +121,7 @@ private:
string field; string field;
} }
SessionVar!(AuthInfo, "authInfo") authInfo = AuthInfo(BsonObjectID.init, SessionVar!(AuthInfo, "authInfo") authInfo = AuthInfo("",
string.init, string.init, Privilege.None); string.init, string.init, Privilege.None);
@Autowire EventStore eventStore; @Autowire EventStore eventStore;

View file

@ -3,11 +3,14 @@ module calendarwebapp.configuration;
import botan.rng.auto_rng : AutoSeededRNG; import botan.rng.auto_rng : AutoSeededRNG;
import botan.rng.rng : RandomNumberGenerator; import botan.rng.rng : RandomNumberGenerator;
import calendarwebapp.authenticator : Authenticator, MongoDBAuthenticator; import calendarwebapp.authenticator : Authenticator, MongoDBAuthenticator,
MySQLAuthenticator;
import calendarwebapp.calendarwebapp : CalendarWebapp; import calendarwebapp.calendarwebapp : CalendarWebapp;
import calendarwebapp.event : EventStore, MongoDBEventStore; import calendarwebapp.event : EventStore, MongoDBEventStore, MySQLEventStore;
import calendarwebapp.passhash : BcryptPasswordHasher, PasswordHasher; import calendarwebapp.passhash : BcryptPasswordHasher, PasswordHasher;
import mysql : MySQLPool;
import poodinis; import poodinis;
import vibe.db.mongo.client : MongoClient; import vibe.db.mongo.client : MongoClient;
@ -20,9 +23,12 @@ public:
override void registerDependencies(shared(DependencyContainer) container) override void registerDependencies(shared(DependencyContainer) container)
{ {
auto mongoClient = connectMongoDB("localhost"); auto mongoClient = connectMongoDB("localhost");
auto pool = new MySQLPool("localhost", "username", "password", "CalendarWebapp");
container.register!MySQLPool.existingInstance(pool);
container.register!MongoClient.existingInstance(mongoClient); container.register!MongoClient.existingInstance(mongoClient);
container.register!(EventStore, MongoDBEventStore!()); container.register!(EventStore, MySQLEventStore);
container.register!(Authenticator, MongoDBAuthenticator!()); container.register!(Authenticator, MySQLAuthenticator);
container.register!(PasswordHasher, BcryptPasswordHasher); container.register!(PasswordHasher, BcryptPasswordHasher);
container.register!(RandomNumberGenerator, AutoSeededRNG); container.register!(RandomNumberGenerator, AutoSeededRNG);
container.register!CalendarWebapp; container.register!CalendarWebapp;

View file

@ -3,6 +3,7 @@ module calendarwebapp.event;
import poodinis; import poodinis;
import std.algorithm : map; import std.algorithm : map;
import std.conv : to;
import std.datetime : Date; import std.datetime : Date;
import std.range.interfaces : InputRange, inputRangeObject; import std.range.interfaces : InputRange, inputRangeObject;
import std.typecons : Nullable; import std.typecons : Nullable;
@ -13,17 +14,17 @@ import vibe.db.mongo.collection : MongoCollection;
interface EventStore interface EventStore
{ {
Event getEvent(BsonObjectID id) @safe; Event getEvent(string id);
InputRange!Event getAllEvents() @safe; InputRange!Event getAllEvents();
void addEvent(Event) @safe; void addEvent(Event);
InputRange!Event getEventsBeginningBetween(Date begin, Date end) @safe; /* InputRange!Event getEventsBeginningBetween(Date begin, Date end) @safe; */
void removeEvent(BsonObjectID id) @safe; void removeEvent(string id);
} }
class MongoDBEventStore(Collection = MongoCollection) : EventStore class MongoDBEventStore(Collection = MongoCollection) : EventStore
{ {
public: public:
Event getEvent(BsonObjectID id) @safe Event getEvent(string id) @safe
{ {
return events.findOne(["_id" : id]).deserializeBson!Event; return events.findOne(["_id" : id]).deserializeBson!Event;
} }
@ -35,20 +36,29 @@ public:
void addEvent(Event event) @safe void addEvent(Event event) @safe
{ {
if (!event.id.valid) import std.conv : ConvException;
event.id = BsonObjectID.generate;
try
{
if (!BsonObjectID.fromString(event.id).valid)
throw new ConvException("invalid BsonObjectID.");
}
catch (ConvException)
{
event.id = BsonObjectID.generate.to!string;
}
events.insert(event.serializeToBson); events.insert(event.serializeToBson);
} }
InputRange!Event getEventsBeginningBetween(Date begin, Date end) @safe InputRange!Event getEventsBeginningBetween(Date begin, Date end) @safe
{ {
return events.find(["$and" : [["date" : ["$gte" : begin.serializeToBson]], ["date" return events.find(["$and" : [["date" : ["$gte" : begin.serializeToBson]],
: ["$lte" : end.serializeToBson]]]]).map!(deserializeBson!Event) ["date" : ["$lte" : end.serializeToBson]]]]).map!(deserializeBson!Event)
.inputRangeObject; .inputRangeObject;
} }
void removeEvent(BsonObjectID id) @safe void removeEvent(string id) @safe
{ {
events.remove(["_id" : id]); events.remove(["_id" : id]);
} }
@ -58,6 +68,80 @@ private:
Collection events; Collection events;
} }
class MySQLEventStore : EventStore
{
private:
import mysql;
public:
Event getEvent(string id)
{
auto cn = pool.lockConnection();
scope (exit)
cn.close;
auto prepared = cn.prepare(
"SELECT id begin end name description type shout FROM events WHERE id = ?");
prepared.setArg(0, id.to!uint);
return toEvent(prepared.query.front);
}
InputRange!Event getAllEvents()
{
auto cn = pool.lockConnection();
scope (exit)
cn.close;
auto prepared = cn.prepare(
"SELECT id, begin, end, name, description, type, shout FROM events");
return prepared.querySet.map!(r => toEvent(r)).inputRangeObject;
}
void addEvent(Event event)
{
auto cn = pool.lockConnection();
scope (exit)
cn.close;
auto prepared = cn.prepare(
"INSERT INTO events (begin, end, name, description, type, shout) VALUES(?, ?, ?, ?, ?, ?)");
prepared.setArgs(event.begin, event.end, event.name, event.description,
event.type.to!uint, event.shout);
prepared.exec();
}
/* InputRange!Event getEventsBeginningBetween(Date begin, Date end) @safe
{
return events.find(["$and" : [["date" : ["$gte" : begin.serializeToBson]],
["date" : ["$lte" : end.serializeToBson]]]]).map!(deserializeBson!Event)
.inputRangeObject;
} */
void removeEvent(string id)
{
auto cn = pool.lockConnection();
scope (exit)
cn.close;
auto prepared = cn.prepare("DELETE FROM events WHERE id = ?");
prepared.setArg(0, id.to!uint);
prepared.exec();
}
private:
@Autowire MySQLPool pool;
Event toEvent(Row r)
{
Event event;
event.id = r[0].get!uint.to!string;
event.begin = r[1].get!Date;
if (r[2].hasValue)
event.end = r[2].get!Date;
event.name = r[3].get!string;
event.description = r[4].get!string;
event.type = r[5].get!uint.to!EventType;
event.shout = r[6].get!byte.to!bool;
return event;
}
}
enum EventType enum EventType
{ {
Holiday, Holiday,
@ -69,7 +153,7 @@ enum EventType
struct Event struct Event
{ {
@serializationName("_id") BsonObjectID id; @serializationName("_id") string id;
@serializationName("date") Date begin; @serializationName("date") Date begin;
@serializationName("end_date") Nullable!Date end; @serializationName("end_date") Nullable!Date end;
string name; string name;

View file

@ -15,7 +15,7 @@ interface Collection
Bson[] find() @safe; Bson[] find() @safe;
Bson findOne(string[string] query) @safe; Bson findOne(string[string] query) @safe;
void insert(Bson document) @safe; void insert(Bson document) @safe;
void remove(BsonObjectID[string] selector) @safe; void remove(string[string] selector) @safe;
} }
class CollectionInjector : ValueInjector!Collection class CollectionInjector : ValueInjector!Collection
@ -46,10 +46,8 @@ public:
RegistrationOption.doNotAddConcreteTypeRegistration); RegistrationOption.doNotAddConcreteTypeRegistration);
container.register!(PasswordHasher, StubPasswordHasher); container.register!(PasswordHasher, StubPasswordHasher);
auto userBson = Bson(["_id" : Bson(BsonObjectID.fromString("5988ef4ae6c19089a1a53b79")), auto userBson = Bson(["_id" : Bson("5988ef4ae6c19089a1a53b79"), "username"
"username" : Bson("foo"), "passwordHash" : Bson("foo"), "passwordHash" : Bson("bar"), "privilege" : Bson(1)]);
: Bson("bar"),
"privilege" : Bson(1)]);
collection.returnValue!"findOne"(Bson(null), userBson, userBson); collection.returnValue!"findOne"(Bson(null), userBson, userBson);

View file

@ -10,15 +10,15 @@ import std.algorithm : map;
import unit_threaded.mock; import unit_threaded.mock;
import unit_threaded.should; import unit_threaded.should;
import vibe.data.bson : Bson, BsonObjectID, serializeToBson; import vibe.data.bson : Bson, serializeToBson;
interface Collection interface Collection
{ {
Bson findOne(BsonObjectID[string] query) @safe; Bson findOne(string[string] query) @safe;
Bson[] find() @safe; Bson[] find() @safe;
Bson[] find(Bson[string][string][][string] query) @safe; Bson[] find(Bson[string][string][][string] query) @safe;
void insert(Bson document) @safe; void insert(Bson document) @safe;
void remove(BsonObjectID[string] selector) @safe; void remove(string[string] selector) @safe;
} }
class CollectionInjector : ValueInjector!Collection class CollectionInjector : ValueInjector!Collection
@ -50,7 +50,7 @@ public:
collection.returnValue!"findOne"(Bson(null)); collection.returnValue!"findOne"(Bson(null));
auto id = BsonObjectID.fromString("599090de97355141140fc698"); auto id = "599090de97355141140fc698";
collection.expect!"findOne"(["_id" : id]); collection.expect!"findOne"(["_id" : id]);
auto eventStore = container.resolve!(EventStore); auto eventStore = container.resolve!(EventStore);
@ -68,7 +68,7 @@ public:
container.register!(EventStore, MongoDBEventStore!(Collection))( container.register!(EventStore, MongoDBEventStore!(Collection))(
RegistrationOption.doNotAddConcreteTypeRegistration); RegistrationOption.doNotAddConcreteTypeRegistration);
auto id = BsonObjectID.fromString("599090de97355141140fc698"); auto id = "599090de97355141140fc698";
Event event; Event event;
event.id = id; event.id = id;
@ -91,7 +91,7 @@ public:
container.register!(EventStore, MongoDBEventStore!(Collection))( container.register!(EventStore, MongoDBEventStore!(Collection))(
RegistrationOption.doNotAddConcreteTypeRegistration); RegistrationOption.doNotAddConcreteTypeRegistration);
auto id = BsonObjectID.fromString("599090de97355141140fc698"); auto id = "599090de97355141140fc698";
Event event; Event event;
event.id = id; event.id = id;
auto serializedEvent = event.serializeToBson; auto serializedEvent = event.serializeToBson;
@ -121,7 +121,7 @@ public:
container.register!(EventStore, MongoDBEventStore!(Collection))( container.register!(EventStore, MongoDBEventStore!(Collection))(
RegistrationOption.doNotAddConcreteTypeRegistration); RegistrationOption.doNotAddConcreteTypeRegistration);
auto id = BsonObjectID.fromString("599090de97355141140fc698"); auto id = "599090de97355141140fc698";
Event event; Event event;
event.id = id; event.id = id;
@ -151,8 +151,7 @@ public:
RegistrationOption.doNotAddConcreteTypeRegistration); RegistrationOption.doNotAddConcreteTypeRegistration);
immutable ids = [ immutable ids = [
BsonObjectID.fromString("599090de97355141140fc698"), BsonObjectID.fromString("599090de97355141140fc698"), "599090de97355141140fc698", "599090de97355141140fc698", "59cb9ad8fc0ba5751c0df02b"
BsonObjectID.fromString("59cb9ad8fc0ba5751c0df02b")
]; ];
auto events = ids.map!(id => Event(id)).array; auto events = ids.map!(id => Event(id)).array;

View file

@ -4,7 +4,6 @@ import calendarwebapp.passhash;
import poodinis; import poodinis;
//import unit_threaded.should;
import unit_threaded; import unit_threaded;
@("BcryptPasswordHasher") @("BcryptPasswordHasher")