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 }