1 /** 2 Database abstraction layer 3 4 Copyright: © 2012 RejectedSoftware e.K. 5 License: Subject to the terms of the General Public License version 3, as written in the included LICENSE.txt file. 6 Authors: Sönke Ludwig 7 */ 8 module userman.controller; 9 10 public import userman.userman; 11 12 import vibe.crypto.passwordhash; 13 import vibe.db.mongo.mongo; 14 import vibe.http.router; 15 import vibe.mail.smtp; 16 import vibe.stream.memory; 17 import vibe.templ.diet; 18 import vibe.utils.validation; 19 20 import std.algorithm; 21 import std.array; 22 import std.datetime; 23 import std.exception; 24 import std.random; 25 import std.string; 26 27 28 class UserManController { 29 private { 30 MongoCollection m_users; 31 MongoCollection m_groups; 32 UserManSettings m_settings; 33 } 34 35 this(UserManSettings settings) 36 { 37 m_settings = settings; 38 39 auto db = connectMongoDB("127.0.0.1").getDatabase(m_settings.databaseName); 40 m_users = db["userman.users"]; 41 m_groups = db["userman.groups"]; 42 43 m_users.ensureIndex(["name": 1], IndexFlags.Unique); 44 m_users.ensureIndex(["email": 1], IndexFlags.Unique); 45 } 46 47 @property UserManSettings settings() { return m_settings; } 48 49 bool isEmailRegistered(string email) 50 { 51 auto bu = m_users.findOne(["email": email], ["auth": true]); 52 return !bu.isNull() && bu.auth.method.get!string.length > 0; 53 } 54 55 void validateUser(User usr) 56 { 57 enforce(usr.name.length > 3, "User names must be at least 3 characters."); 58 validateEmail(usr.email); 59 } 60 61 void addUser(User usr) 62 { 63 validateUser(usr); 64 enforce(m_users.findOne(["name": usr.name]).isNull(), "The user name is already taken."); 65 enforce(m_users.findOne(["email": usr.email]).isNull(), "The email address is already in use."); 66 usr._id = BsonObjectID.generate(); 67 m_users.insert(usr); 68 } 69 70 BsonObjectID registerUser(string email, string name, string full_name, string password) 71 { 72 email = email.toLower(); 73 name = name.toLower(); 74 75 validateEmail(email); 76 validatePassword(password, password); 77 78 auto need_activation = m_settings.requireAccountValidation; 79 auto user = new User; 80 user._id = BsonObjectID.generate(); 81 user.active = !need_activation; 82 user.name = name; 83 user.fullName = full_name; 84 user.auth.method = "password"; 85 user.auth.passwordHash = generateSimplePasswordHash(password); 86 user.email = email; 87 if( need_activation ) 88 user.activationCode = generateActivationCode(); 89 addUser(user); 90 91 if( need_activation ) 92 resendActivation(email); 93 94 return user._id; 95 } 96 97 BsonObjectID inviteUser(string email, string full_name, string message) 98 { 99 email = email.toLower(); 100 101 validateEmail(email); 102 103 auto existing = m_users.findOne(["email": email], ["_id": true]); 104 if( !existing.isNull() ) return existing._id.get!BsonObjectID; 105 106 auto user = new User; 107 user._id = BsonObjectID.generate(); 108 user.email = email; 109 user.name = email; 110 user.fullName = full_name; 111 addUser(user); 112 113 if( m_settings.mailSettings ){ 114 auto msg = new MemoryOutputStream; 115 parseDietFileCompat!("userman.mail.invitation.dt", 116 User, "user", 117 string, "serviceName", 118 URL, "serviceUrl")(msg, 119 user, 120 m_settings.serviceName, 121 m_settings.serviceUrl); 122 123 auto mail = new Mail; 124 mail.headers["From"] = m_settings.serviceName ~ " <" ~ m_settings.serviceEmail ~ ">"; 125 mail.headers["To"] = email; 126 mail.headers["Subject"] = "Invitation"; 127 mail.headers["Content-Type"] = "text/html; charset=UTF-8"; 128 mail.bodyText = cast(string)msg.data(); 129 130 sendMail(m_settings.mailSettings, mail); 131 } 132 133 return user._id; 134 } 135 136 void activateUser(string email, string activation_code) 137 { 138 email = email.toLower(); 139 140 auto busr = m_users.findOne(["email": email]); 141 enforce(!busr.isNull(), "There is no user account for the specified email address."); 142 enforce(!busr.active, "This user account is already activated."); 143 enforce(busr.activationCode.get!string == activation_code, "The activation code provided is not valid."); 144 busr.active = true; 145 busr.activationCode = ""; 146 m_users.update(["_id": busr._id], busr); 147 } 148 149 void resendActivation(string email) 150 { 151 email = email.toLower(); 152 153 auto busr = m_users.findOne(["email": email]); 154 enforce(!busr.isNull(), "There is no user account for the specified email address."); 155 enforce(!busr.active, "The user account is already active."); 156 157 auto user = new User; 158 deserializeBson(user, busr); 159 160 auto msg = new MemoryOutputStream; 161 parseDietFileCompat!("userman.mail.activation.dt", 162 User, "user", 163 string, "serviceName", 164 URL, "serviceUrl")(msg, 165 user, 166 m_settings.serviceName, 167 m_settings.serviceUrl); 168 169 auto mail = new Mail; 170 mail.headers["From"] = m_settings.serviceName ~ " <" ~ m_settings.serviceEmail ~ ">"; 171 mail.headers["To"] = email; 172 mail.headers["Subject"] = "Account activation"; 173 mail.headers["Content-Type"] = "text/html; charset=UTF-8"; 174 mail.bodyText = cast(string)msg.data(); 175 176 sendMail(m_settings.mailSettings, mail); 177 } 178 179 void requestPasswordReset(string email) 180 { 181 auto usr = getUserByEmail(email); 182 183 string reset_code = generateActivationCode(); 184 BsonDate expire_time = BsonDate(Clock.currTime() + dur!"hours"(24)); 185 m_users.update(["_id": usr._id], ["$set": ["resetCode": Bson(reset_code), "resetCodeExpireTime": Bson(expire_time)]]); 186 187 if( m_settings.mailSettings ){ 188 auto msg = new MemoryOutputStream; 189 parseDietFileCompat!("userman.mail.reset_password.dt", 190 User*, "user", 191 string, "reset_code", 192 UserManSettings, "settings") 193 (msg, &usr, reset_code, m_settings); 194 195 auto mail = new Mail; 196 mail.headers["From"] = m_settings.serviceName ~ " <" ~ m_settings.serviceEmail ~ ">"; 197 mail.headers["To"] = email; 198 mail.headers["Subject"] = "Account recovery"; 199 mail.headers["Content-Type"] = "text/html; charset=UTF-8"; 200 mail.bodyText = cast(string)msg.data(); 201 sendMail(m_settings.mailSettings, mail); 202 } 203 } 204 205 void resetPassword(string email, string reset_code, string new_password) 206 { 207 validatePassword(new_password, new_password); 208 auto usr = getUserByEmail(email); 209 enforce(usr.resetCode.length > 0, "No password reset request was made."); 210 enforce(Clock.currTime() < usr.resetCodeExpireTime.toSysTime(), "Reset code is expired, please request a new one."); 211 m_users.update(["_id": usr._id], ["$set": ["resetCode": ""]]); 212 auto code = usr.resetCode; 213 enforce(reset_code == code, "Invalid request code, please request a new one."); 214 m_users.update(["_id": usr._id], ["$set": ["auth.passwordHash": generateSimplePasswordHash(new_password)]]); 215 } 216 217 User getUser(BsonObjectID id) 218 { 219 auto busr = m_users.findOne(["_id": id]); 220 enforce(!busr.isNull(), "The specified user id is invalid."); 221 auto ret = new User; 222 deserializeBson(ret, busr); 223 return ret; 224 } 225 226 User getUserByName(string name) 227 { 228 name = name.toLower(); 229 230 auto busr = m_users.findOne(["name": name]); 231 enforce(!busr.isNull(), "The specified user name is not registered."); 232 auto ret = new User; 233 deserializeBson(ret, busr); 234 return ret; 235 } 236 237 User getUserByEmail(string email) 238 { 239 email = email.toLower(); 240 241 auto busr = m_users.findOne(["email": email]); 242 enforce(!busr.isNull(), "The specified email address is not registered."); 243 auto ret = new User; 244 deserializeBson(ret, busr); 245 return ret; 246 } 247 248 User getUserByEmailOrName(string email_or_name) 249 { 250 auto busr = m_users.findOne(["$or": [["email": email_or_name.toLower()], ["name": email_or_name]]]); 251 enforce(!busr.isNull(), "The specified email address or user name is not registered."); 252 auto ret = new User; 253 deserializeBson(ret, busr); 254 return ret; 255 } 256 257 void enumerateUsers(int first_user, int max_count, void delegate(ref User usr) del) 258 { 259 foreach( busr; m_users.find(["query": null, "orderby": ["name": 1]], null, QueryFlags.None, first_user, max_count) ){ 260 if (max_count-- <= 0) break; 261 auto usr = deserializeBson!User(busr); 262 del(usr); 263 } 264 } 265 266 long getUserCount() 267 { 268 return m_users.count(Bson.emptyObject); 269 } 270 271 void deleteUser(BsonObjectID user_id) 272 { 273 m_users.remove(["_id": user_id]); 274 } 275 276 void updateUser(User user) 277 { 278 validateUser(user); 279 enforce(m_settings.useUserNames || user.name == user.email, "User name must equal email address if user names are not used."); 280 281 m_users.update(["_id": user._id], user); 282 } 283 284 void addGroup(string name, string description) 285 { 286 enforce(m_groups.findOne(["name": name]).isNull(), "A group with this name already exists."); 287 auto grp = new Group; 288 grp._id = BsonObjectID.generate(); 289 grp.name = name; 290 grp.description = description; 291 m_groups.insert(grp); 292 } 293 } 294 295 class User { 296 BsonObjectID _id; 297 bool active; 298 bool banned; 299 string name; 300 string fullName; 301 string email; 302 string[] groups; 303 string activationCode; 304 string resetCode; 305 BsonDate resetCodeExpireTime; 306 AuthInfo auth; 307 Bson[string] properties; 308 309 bool isInGroup(string name) const { return groups.countUntil(name) >= 0; } 310 } 311 312 struct AuthInfo { 313 string method = "password"; 314 string passwordHash; 315 string token; 316 string secret; 317 string info; 318 } 319 320 class Group { 321 BsonObjectID _id; 322 string name; 323 string description; 324 } 325 326 string generateActivationCode() 327 { 328 auto ret = appender!string(); 329 foreach( i; 0 .. 10 ){ 330 auto n = cast(char)uniform(0, 62); 331 if( n < 26 ) ret.put(cast(char)('a'+n)); 332 else if( n < 52 ) ret.put(cast(char)('A'+n-26)); 333 else ret.put(cast(char)('0'+n-52)); 334 } 335 return ret.data(); 336 }