diff --git a/dub.json b/dub.json index 74add46..cf00d50 100644 --- a/dub.json +++ b/dub.json @@ -4,6 +4,7 @@ "Johannes Loher" ], "dependencies": { + "mysql-native": "~>1.1.2", "botan": "~>1.12.9", "vibe-d": "~>0.8.1", "vibe-d:tls": "~>0.8.1", diff --git a/schema.sql b/schema.sql new file mode 100644 index 0000000..e9ae829 --- /dev/null +++ b/schema.sql @@ -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); diff --git a/source/calendarwebapp/authenticator.d b/source/calendarwebapp/authenticator.d index 5f79986..09d8d56 100644 --- a/source/calendarwebapp/authenticator.d +++ b/source/calendarwebapp/authenticator.d @@ -4,18 +4,20 @@ 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 { Nullable!AuthInfo checkUser(string username, string password) @safe; - void addUser(AuthInfo authInfo) @safe; - InputRange!AuthInfo getAllUsers() @safe; - void removeUser(BsonObjectID id) @safe; + void addUser(AuthInfo authInfo); + InputRange!AuthInfo getAllUsers(); + void removeUser(string id); } class MongoDBAuthenticator(Collection = MongoCollection) : Authenticator @@ -47,8 +49,17 @@ public: void addUser(AuthInfo authInfo) @safe { - if (!authInfo.id.valid) - authInfo.id = BsonObjectID.generate; + 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); } @@ -61,7 +72,7 @@ public: return users.find().map!(deserializeBson!AuthInfo).inputRangeObject; } - void removeUser(BsonObjectID id) @safe + void removeUser(string id) @safe { users.remove(["_id" : id]); } @@ -74,11 +85,91 @@ enum Privilege 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 { import vibe.data.serialization : name; - @name("_id") BsonObjectID id; + @name("_id") string id; string username; string passwordHash; Privilege privilege; diff --git a/source/calendarwebapp/calendarwebapp.d b/source/calendarwebapp/calendarwebapp.d index 5757bfb..fdc958f 100644 --- a/source/calendarwebapp/calendarwebapp.d +++ b/source/calendarwebapp/calendarwebapp.d @@ -65,23 +65,23 @@ public: render!("createevent.dt", _error, authInfo); } + @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; if (!end.isNull) enforce(end - begin >= 1.days, "Mehrtägige Ereignisse müssen mindestens einen Tag dauern"); - auto event = Event(BsonObjectID.generate, begin, end, name, - description.replace("\r", ""), type, shout); + auto event = Event("", begin, end, name, description.replace("\r", ""), type, shout); eventStore.addEvent(event); redirect("/"); } - @auth(Role.user | Role.admin) void postRemoveevent(BsonObjectID id) @safe + @auth(Role.user | Role.admin) void postRemoveevent(string id) { eventStore.removeEvent(id); redirect("/"); @@ -94,7 +94,7 @@ public: render!("showusers.dt", users, authInfo); } - @auth(Role.admin) void postRemoveuser(BsonObjectID id) @safe + @auth(Role.admin) void postRemoveuser(string id) { authenticator.removeUser(id); redirect("/users"); @@ -107,9 +107,9 @@ public: } @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)); redirect("/users"); } @@ -121,7 +121,7 @@ private: string field; } - SessionVar!(AuthInfo, "authInfo") authInfo = AuthInfo(BsonObjectID.init, + SessionVar!(AuthInfo, "authInfo") authInfo = AuthInfo("", string.init, string.init, Privilege.None); @Autowire EventStore eventStore; diff --git a/source/calendarwebapp/configuration.d b/source/calendarwebapp/configuration.d index 21a5465..ddf38af 100644 --- a/source/calendarwebapp/configuration.d +++ b/source/calendarwebapp/configuration.d @@ -3,11 +3,14 @@ module calendarwebapp.configuration; import botan.rng.auto_rng : AutoSeededRNG; import botan.rng.rng : RandomNumberGenerator; -import calendarwebapp.authenticator : Authenticator, MongoDBAuthenticator; +import calendarwebapp.authenticator : Authenticator, MongoDBAuthenticator, + MySQLAuthenticator; import calendarwebapp.calendarwebapp : CalendarWebapp; -import calendarwebapp.event : EventStore, MongoDBEventStore; +import calendarwebapp.event : EventStore, MongoDBEventStore, MySQLEventStore; import calendarwebapp.passhash : BcryptPasswordHasher, PasswordHasher; +import mysql : MySQLPool; + import poodinis; import vibe.db.mongo.client : MongoClient; @@ -20,9 +23,12 @@ public: override void registerDependencies(shared(DependencyContainer) container) { auto mongoClient = connectMongoDB("localhost"); + auto pool = new MySQLPool("localhost", "username", "password", "CalendarWebapp"); + container.register!MySQLPool.existingInstance(pool); container.register!MongoClient.existingInstance(mongoClient); - container.register!(EventStore, MongoDBEventStore!()); - container.register!(Authenticator, MongoDBAuthenticator!()); + container.register!(EventStore, MySQLEventStore); + container.register!(Authenticator, MySQLAuthenticator); + container.register!(PasswordHasher, BcryptPasswordHasher); container.register!(RandomNumberGenerator, AutoSeededRNG); container.register!CalendarWebapp; diff --git a/source/calendarwebapp/event.d b/source/calendarwebapp/event.d index e03ee43..bde695d 100644 --- a/source/calendarwebapp/event.d +++ b/source/calendarwebapp/event.d @@ -3,6 +3,7 @@ module calendarwebapp.event; import poodinis; import std.algorithm : map; +import std.conv : to; import std.datetime : Date; import std.range.interfaces : InputRange, inputRangeObject; import std.typecons : Nullable; @@ -13,17 +14,17 @@ import vibe.db.mongo.collection : MongoCollection; interface EventStore { - Event getEvent(BsonObjectID id) @safe; - InputRange!Event getAllEvents() @safe; - void addEvent(Event) @safe; - InputRange!Event getEventsBeginningBetween(Date begin, Date end) @safe; - void removeEvent(BsonObjectID id) @safe; + Event getEvent(string id); + InputRange!Event getAllEvents(); + void addEvent(Event); + /* InputRange!Event getEventsBeginningBetween(Date begin, Date end) @safe; */ + void removeEvent(string id); } class MongoDBEventStore(Collection = MongoCollection) : EventStore { public: - Event getEvent(BsonObjectID id) @safe + Event getEvent(string id) @safe { return events.findOne(["_id" : id]).deserializeBson!Event; } @@ -35,20 +36,29 @@ public: void addEvent(Event event) @safe { - if (!event.id.valid) - event.id = BsonObjectID.generate; + import std.conv : ConvException; + + try + { + if (!BsonObjectID.fromString(event.id).valid) + throw new ConvException("invalid BsonObjectID."); + } + catch (ConvException) + { + event.id = BsonObjectID.generate.to!string; + } events.insert(event.serializeToBson); } InputRange!Event getEventsBeginningBetween(Date begin, Date end) @safe { - return events.find(["$and" : [["date" : ["$gte" : begin.serializeToBson]], ["date" - : ["$lte" : end.serializeToBson]]]]).map!(deserializeBson!Event) + return events.find(["$and" : [["date" : ["$gte" : begin.serializeToBson]], + ["date" : ["$lte" : end.serializeToBson]]]]).map!(deserializeBson!Event) .inputRangeObject; } - void removeEvent(BsonObjectID id) @safe + void removeEvent(string id) @safe { events.remove(["_id" : id]); } @@ -58,6 +68,80 @@ private: 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 { Holiday, @@ -69,7 +153,7 @@ enum EventType struct Event { - @serializationName("_id") BsonObjectID id; + @serializationName("_id") string id; @serializationName("date") Date begin; @serializationName("end_date") Nullable!Date end; string name; diff --git a/test/calendarwebapp/testauthenticator.d b/test/calendarwebapp/testauthenticator.d index c1450be..4276e42 100644 --- a/test/calendarwebapp/testauthenticator.d +++ b/test/calendarwebapp/testauthenticator.d @@ -15,7 +15,7 @@ interface Collection Bson[] find() @safe; Bson findOne(string[string] query) @safe; void insert(Bson document) @safe; - void remove(BsonObjectID[string] selector) @safe; + void remove(string[string] selector) @safe; } class CollectionInjector : ValueInjector!Collection @@ -46,10 +46,8 @@ public: RegistrationOption.doNotAddConcreteTypeRegistration); container.register!(PasswordHasher, StubPasswordHasher); - auto userBson = Bson(["_id" : Bson(BsonObjectID.fromString("5988ef4ae6c19089a1a53b79")), - "username" : Bson("foo"), "passwordHash" - : Bson("bar"), - "privilege" : Bson(1)]); + auto userBson = Bson(["_id" : Bson("5988ef4ae6c19089a1a53b79"), "username" + : Bson("foo"), "passwordHash" : Bson("bar"), "privilege" : Bson(1)]); collection.returnValue!"findOne"(Bson(null), userBson, userBson); diff --git a/test/calendarwebapp/testevent.d b/test/calendarwebapp/testevent.d index e351881..4f0024b 100644 --- a/test/calendarwebapp/testevent.d +++ b/test/calendarwebapp/testevent.d @@ -10,15 +10,15 @@ import std.algorithm : map; import unit_threaded.mock; import unit_threaded.should; -import vibe.data.bson : Bson, BsonObjectID, serializeToBson; +import vibe.data.bson : Bson, serializeToBson; interface Collection { - Bson findOne(BsonObjectID[string] query) @safe; + Bson findOne(string[string] query) @safe; Bson[] find() @safe; Bson[] find(Bson[string][string][][string] query) @safe; void insert(Bson document) @safe; - void remove(BsonObjectID[string] selector) @safe; + void remove(string[string] selector) @safe; } class CollectionInjector : ValueInjector!Collection @@ -50,7 +50,7 @@ public: collection.returnValue!"findOne"(Bson(null)); - auto id = BsonObjectID.fromString("599090de97355141140fc698"); + auto id = "599090de97355141140fc698"; collection.expect!"findOne"(["_id" : id]); auto eventStore = container.resolve!(EventStore); @@ -68,7 +68,7 @@ public: container.register!(EventStore, MongoDBEventStore!(Collection))( RegistrationOption.doNotAddConcreteTypeRegistration); - auto id = BsonObjectID.fromString("599090de97355141140fc698"); + auto id = "599090de97355141140fc698"; Event event; event.id = id; @@ -91,7 +91,7 @@ public: container.register!(EventStore, MongoDBEventStore!(Collection))( RegistrationOption.doNotAddConcreteTypeRegistration); - auto id = BsonObjectID.fromString("599090de97355141140fc698"); + auto id = "599090de97355141140fc698"; Event event; event.id = id; auto serializedEvent = event.serializeToBson; @@ -121,7 +121,7 @@ public: container.register!(EventStore, MongoDBEventStore!(Collection))( RegistrationOption.doNotAddConcreteTypeRegistration); - auto id = BsonObjectID.fromString("599090de97355141140fc698"); + auto id = "599090de97355141140fc698"; Event event; event.id = id; @@ -151,8 +151,7 @@ public: RegistrationOption.doNotAddConcreteTypeRegistration); immutable ids = [ - BsonObjectID.fromString("599090de97355141140fc698"), BsonObjectID.fromString("599090de97355141140fc698"), - BsonObjectID.fromString("59cb9ad8fc0ba5751c0df02b") + "599090de97355141140fc698", "599090de97355141140fc698", "59cb9ad8fc0ba5751c0df02b" ]; auto events = ids.map!(id => Event(id)).array; diff --git a/test/calendarwebapp/testpasshash.d b/test/calendarwebapp/testpasshash.d index 6282db5..634a7e9 100644 --- a/test/calendarwebapp/testpasshash.d +++ b/test/calendarwebapp/testpasshash.d @@ -4,7 +4,6 @@ import calendarwebapp.passhash; import poodinis; -//import unit_threaded.should; import unit_threaded; @("BcryptPasswordHasher")