Browse Source

Added Social Authentication + fixed Agent Cron

NGPixel 8 years ago
parent
commit
dc6fc449f0

+ 0 - 16
.snyk

@@ -1,16 +0,0 @@
-failThreshold: high
-version: v1.5.2
-ignore:
-  'npm:moment:20160126':
-    - express-brute-mongo > moment:
-        reason: None given
-        expires: '2016-11-16T22:23:46.921Z'
-patch:
-  'npm:negotiator:20160616':
-    - socket.io > engine.io > accepts > negotiator:
-        patched: '2016-09-09T02:19:31.082Z'
-  'npm:ws:20160624':
-    - socket.io > engine.io > ws:
-        patched: '2016-09-09T02:19:31.082Z'
-    - socket.io > socket.io-client > engine.io-client > ws:
-        patched: '2016-09-09T02:19:31.082Z'

+ 9 - 0
README.md

@@ -20,13 +20,22 @@
 
 
 ##### Milestones
 ##### Milestones
 - [ ] Assets Management
 - [ ] Assets Management
+	- [x] Images
+	- [ ] Files/Documents
 - [ ] Authentication
 - [ ] Authentication
+	- [ ] Local
+	- [x] Microsoft Account
+	- [x] Google ID
+	- [x] Facebook
 - [x] Background Agent (git sync, cache purge, etc.)
 - [x] Background Agent (git sync, cache purge, etc.)
 - [x] Caching
 - [x] Caching
 - [x] Create Entry
 - [x] Create Entry
 - [x] Edit Entry
 - [x] Edit Entry
 - [x] Git Management
 - [x] Git Management
 - [ ] Markdown Editor
 - [ ] Markdown Editor
+	- [x] Basic Formatting
+	- [ ] Links
+	- [ ] Table Editor
 - [x] Move Entry
 - [x] Move Entry
 - [x] Navigation
 - [x] Navigation
 - [x] Parsing / Tree / Metadata
 - [x] Parsing / Tree / Metadata

+ 3 - 1
agent.js

@@ -167,7 +167,9 @@ var job = new cron({
 
 
 			if(!jobUplWatchStarted) {
 			if(!jobUplWatchStarted) {
 				jobUplWatchStarted = true;
 				jobUplWatchStarted = true;
-				upl.initialScan();
+				upl.initialScan().then(() => {
+					job.start();
+				});
 			}
 			}
 
 
 			return true;
 			return true;

File diff suppressed because it is too large
+ 0 - 0
assets/css/app.css


File diff suppressed because it is too large
+ 0 - 0
assets/css/libs.css


File diff suppressed because it is too large
+ 0 - 0
assets/css/login.css


BIN
assets/images/bg_2.jpg


BIN
assets/images/bg_3.jpg


File diff suppressed because it is too large
+ 0 - 0
assets/js/libs.js


+ 1 - 0
client/scss/components/_editor.scss

@@ -205,6 +205,7 @@
 	border-left: none;
 	border-left: none;
 	border-right: none;
 	border-right: none;
 	padding-top: 52px;
 	padding-top: 52px;
+	font-family: $family-monospace;
 }
 }
 
 
 .CodeMirror .CodeMirror-code .cm-url {
 .CodeMirror .CodeMirror-code .cm-url {

+ 27 - 1
client/scss/layout/_content.scss

@@ -36,6 +36,13 @@
 .mkcontent {
 .mkcontent {
 
 
 	h1 {
 	h1 {
+		border-bottom: 1px dotted $blue;
+		padding-bottom: 4px;
+		font-weight: 400;
+		color: desaturate($blue, 20%);
+	}
+
+	h2 {
 		border-bottom: 1px dotted $grey-light;
 		border-bottom: 1px dotted $grey-light;
 		padding-bottom: 4px;
 		padding-bottom: 4px;
 		font-weight: 400;
 		font-weight: 400;
@@ -44,7 +51,7 @@
 
 
 	a.toc-anchor {
 	a.toc-anchor {
 		font-size: 80%;
 		font-size: 80%;
-		color: $purple;
+		color: $blue;
 		border-bottom: none;
 		border-bottom: none;
 
 
 		&:visited {
 		&:visited {
@@ -74,6 +81,7 @@
 
 
 	pre {
 	pre {
 		padding: 0;
 		padding: 0;
+		font-family: $family-monospace;
 
 
 		> code {
 		> code {
 			box-shadow: inset 0 0 5px 0 $grey-light;
 			box-shadow: inset 0 0 5px 0 $grey-light;
@@ -94,6 +102,7 @@
 		float: right;
 		float: right;
 		margin-top: -50px;
 		margin-top: -50px;
 		max-width: 200px;
 		max-width: 200px;
+		background-color: #FFF;
 	}
 	}
 
 
 	strong {
 	strong {
@@ -104,6 +113,23 @@
 		font-size: 120%;
 		font-size: 120%;
 	}
 	}
 
 
+	table thead th {
+		background-color: $blue;
+		color: #FFF;
+		border-color: #FFF;
+		border-bottom-color: $blue;
+		border-top-color: $blue;
+
+		&:first-child {
+			border-left-color: $blue;
+		}
+
+		&:last-child {
+			border-right-color: $blue;
+		}
+
+	}
+
 }
 }
 
 
 .content a:not(.button):visited {
 .content a:not(.button):visited {

+ 1 - 1
client/scss/libs/bulma/utilities/variables.sass

@@ -20,7 +20,7 @@ $yellow: #fce473 !default
 
 
 // Typography
 // Typography
 
 
-$family-monospace: monospace;
+$family-monospace: Consolas, "Liberation Mono", Menlo, Courier, monospace;
 $family-sans-serif: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol";
 $family-sans-serif: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol";
 
 
 $size-1: 48px !default
 $size-1: 48px !default

+ 53 - 0
client/scss/login.scss

@@ -125,6 +125,59 @@ a {
 
 
 	}
 	}
 
 
+	#social {
+		margin-top: 25px;
+
+		> span {
+			display: block;
+			font-weight: bold;
+			color: rgba(255,255,255,0.7);
+		}
+
+		button {
+			margin-right: 5px;
+			width: auto;
+			padding: 0 15px;
+
+			> i {
+				margin-right: 10px;
+				font-size: 16px;
+			}
+
+			&.ms {
+				background-color: #009688;
+				border-color: lighten(#009688, 10%);
+
+				&:hover {
+					background-color: darken(#009688, 10%);
+				}
+
+			}
+
+			&.google {
+				background-color: #2196F3;
+				border-color: lighten(#2196F3, 10%);
+
+				&:hover {
+					background-color: darken(#2196F3, 10%);
+				}
+
+			}
+
+			&.facebook {
+				background-color: #673AB7;
+				border-color: lighten(#673AB7, 10%);
+
+				&:hover {
+					background-color: darken(#673AB7, 10%);
+				}
+
+			}
+
+		}
+
+	}
+
 }
 }
 
 
 #copyright {
 #copyright {

+ 20 - 0
config.sample.yml

@@ -31,6 +31,26 @@ paths:
   repo: ./repo
   repo: ./repo
   data: ./data
   data: ./data
 
 
+# ---------------------------------------------------------------------
+# Site Authentication
+# ---------------------------------------------------------------------
+
+auth:
+  local:
+    enabled: true
+  google:
+    enabled: true
+    clientId: GOOGLE_CLIENT_ID
+    clientSecret: GOOGLE_CLIENT_SECRET
+  microsoft:
+    enabled: true
+    clientId: MS_APP_ID
+    clientSecret: MS_APP_SECRET
+  facebook:
+    enabled: false
+    clientId: FACEBOOK_APP_ID
+    clientSecret: FACEBOOK_APP_SECRET
+
 # ---------------------------------------------------------------------
 # ---------------------------------------------------------------------
 # Database Connection String
 # Database Connection String
 # ---------------------------------------------------------------------
 # ---------------------------------------------------------------------

+ 12 - 0
controllers/auth.js

@@ -59,6 +59,18 @@ router.post('/login', bruteforce.prevent, function(req, res, next) {
 		})(req, res, next);
 		})(req, res, next);
 });
 });
 
 
+/**
+ * Social Login
+ */
+
+router.get('/login/ms', passport.authenticate('windowslive', { scope: ['wl.signin', 'wl.basic', 'wl.emails'] }));
+router.get('/login/google', passport.authenticate('google', { scope: ['profile', 'email'] }));
+router.get('/login/facebook', passport.authenticate('facebook', { scope: ['public_profile', 'email'] }));
+
+router.get('/login/ms/callback', passport.authenticate('windowslive', { failureRedirect: '/login', successRedirect: '/' }));
+router.get('/login/google/callback', passport.authenticate('google', { failureRedirect: '/login', successRedirect: '/' }));
+router.get('/login/facebook/callback', passport.authenticate('facebook', { failureRedirect: '/login', successRedirect: '/' }));
+
 /**
 /**
  * Logout
  * Logout
  */
  */

+ 2 - 0
controllers/ws.js

@@ -1,6 +1,8 @@
 "use strict";
 "use strict";
 
 
 module.exports = (socket) => {
 module.exports = (socket) => {
+  
+  console.log(socket.request.user);
 
 
   //-----------------------------------------
   //-----------------------------------------
   // SEARCH
   // SEARCH

+ 143 - 61
libs/auth.js

@@ -1,66 +1,148 @@
-var LocalStrategy = require('passport-local').Strategy;
+"use strict";
+
+const LocalStrategy = require('passport-local').Strategy;
+const GoogleStrategy = require('passport-google-oauth20').Strategy;
+const WindowsLiveStrategy = require('passport-windowslive').Strategy;
+const FacebookStrategy = require('passport-facebook').Strategy;
+const _ = require('lodash');
 
 
 module.exports = function(passport, appconfig) {
 module.exports = function(passport, appconfig) {
 
 
-    // Serialization user methods
-
-    passport.serializeUser(function(user, done) {
-        done(null, user._id);
-    });
-
-    passport.deserializeUser(function(id, done) {
-        let user = db.User.find({ id });
-        if(user) {
-            done(null, user);
-        } else {
-            done(err, null);
-        }
-    });
-
-    // Setup local user authentication strategy
-
-    passport.use(
-        'local',
-        new LocalStrategy({
-            usernameField : 'email',
-            passwordField : 'password',
-            passReqToCallback : true
-        },
-        function(req, uEmail, uPassword, done) {
-            db.User.findOne({ 'email' :  uEmail }).then((user) => {
-                if (user) {
-                    user.validatePassword(uPassword).then((isValid) => {
-                        return (isValid) ? done(null, user) : done(null, false);
-                    });
-                } else {
-                    return done(null, false);
-                }
-            }).catch((err) => {
-                done(err);
-            });
-        })
-    );
-
-    // Check for admin access
-
-    db.onReady.then(() => {
-
-        /*if(db.User.count() < 1) {
-            winston.info('No administrator account found. Creating a new one...');
-            if(db.User.insert({
-                email: appconfig.admin,
-                firstName: "Admin",
-                lastName: "Admin",
-                password: "admin123"
-            })) {
-                winston.info('Administrator account created successfully!');
-            } else {
-                winston.error('An error occured while creating administrator account: ');
-            }
-        }*/
-
-        return true;
-
-    });
+	// Serialization user methods
+
+	passport.serializeUser(function(user, done) {
+		done(null, user._id);
+	});
+
+	passport.deserializeUser(function(id, done) {
+		db.User.findById(id).then((user) => {
+			if(user) {
+				done(null, user);
+			} else {
+				done(new Error('User not found.'), null);
+			}
+			return true;
+		}).catch((err) => {
+			done(err, null);
+		});
+	});
+
+	// Local Account
+
+	if(appconfig.auth.local && appconfig.auth.local.enabled) {
+
+		passport.use('local',
+			new LocalStrategy({
+				usernameField : 'email',
+				passwordField : 'password',
+				passReqToCallback : true
+			},
+			function(req, uEmail, uPassword, done) {
+				db.User.findOne({ 'email' :  uEmail }).then((user) => {
+					if (user) {
+						user.validatePassword(uPassword).then((isValid) => {
+							return (isValid) ? done(null, user) : done(null, false);
+						});
+					} else {
+						return done(null, false);
+					}
+				}).catch((err) => {
+					done(err);
+				});
+			})
+		);
+
+	}
+
+	// Google ID
+
+	if(appconfig.auth.google && appconfig.auth.google.enabled) {
+
+		passport.use('google',
+			new GoogleStrategy({
+				clientID: appconfig.auth.google.clientId,
+				clientSecret: appconfig.auth.google.clientSecret,
+				callbackURL: appconfig.host + '/login/google/callback'
+		  },
+		  (accessToken, refreshToken, profile, cb) => {
+		  	db.User.processProfile(profile).then((user) => {
+		  		return cb(null, user) || true;
+		  	}).catch((err) => {
+		  		return cb(err, null) || true;
+		  	});
+		  }
+		));
+
+	}
+
+	// Microsoft Accounts
+
+	if(appconfig.auth.microsoft && appconfig.auth.microsoft.enabled) {
+
+		passport.use('windowslive',
+			new WindowsLiveStrategy({
+				clientID: appconfig.auth.microsoft.clientId,
+				clientSecret: appconfig.auth.microsoft.clientSecret,
+				callbackURL: appconfig.host + '/login/ms/callback'
+		  },
+		  function(accessToken, refreshToken, profile, cb) {
+		  	db.User.processProfile(profile).then((user) => {
+		  		return cb(null, user) || true;
+		  	}).catch((err) => {
+		  		return cb(err, null) || true;
+		  	});
+		  }
+		));
+
+	}
+
+	// Facebook
+
+	if(appconfig.auth.facebook && appconfig.auth.facebook.enabled) {
+
+		passport.use('facebook',
+			new FacebookStrategy({
+				clientID: appconfig.auth.facebook.clientId,
+				clientSecret: appconfig.auth.facebook.clientSecret,
+				callbackURL: appconfig.host + '/login/facebook/callback',
+				profileFields: ['id', 'displayName', 'email']
+		  },
+		  function(accessToken, refreshToken, profile, cb) {
+		  	db.User.processProfile(profile).then((user) => {
+		  		return cb(null, user) || true;
+		  	}).catch((err) => {
+		  		return cb(err, null) || true;
+		  	});
+		  }
+		));
+
+	}
+
+	// Check for admin access
+
+	db.onReady.then(() => {
+
+		db.User.count().then((c) => {
+			if(c < 1) {
+				winston.info('[' + PROCNAME + '][AUTH] No administrator account found. Creating a new one...');
+				db.User.hashPassword('admin123').then((pwd) => {
+					return db.User.create({
+						provider: 'local',
+						email: appconfig.admin,
+						name: "Administrator",
+						password: pwd
+					});
+				}).then(() => {
+					winston.info('[' + PROCNAME + '][AUTH] Administrator account created successfully!');
+				}).catch((err) => {
+					winston.error('[' + PROCNAME + '][AUTH] An error occured while creating administrator account:');
+					winston.error(err);
+				});
+			}
+		});
+		
+		return true;
+
+	});
 
 
 };
 };

+ 23 - 2
libs/config.js

@@ -21,15 +21,36 @@ module.exports = (confPath) => {
 	  process.exit(1);
 	  process.exit(1);
 	}
 	}
 
 
-	return _.defaultsDeep(appconfig, {
+	// Merge with defaults
+
+	appconfig = _.defaultsDeep(appconfig, {
 		title: "Requarks Wiki",
 		title: "Requarks Wiki",
 		host: "http://localhost",
 		host: "http://localhost",
 		port: process.env.PORT,
 		port: process.env.PORT,
-		wsPort: 8080,
+		auth: {
+			local: { enabled: true },
+			microsoft: { enabled: false },
+			google: { enabled: false },
+			facebook: { enabled: false },
+		},
 		db: "mongodb://localhost/wiki",
 		db: "mongodb://localhost/wiki",
 		redis: null,
 		redis: null,
 		sessionSecret: null,
 		sessionSecret: null,
 		admin: null
 		admin: null
 	});
 	});
 
 
+	// List authentication strategies
+	
+	appconfig.authStrategies = {
+		list: _.filter(appconfig.auth, ['enabled', true]),
+		socialEnabled: (_.chain(appconfig.auth).omit('local').reject({ enabled: false }).value().length > 0)
+	}
+	if(appconfig.authStrategies.list.length < 1) {
+		winston.error(new Error('You must enable at least 1 authentication strategy!'));
+	  process.exit(1);
+	}
+
+
+	return appconfig;
+
 };
 };

+ 1 - 1
libs/winston.js

@@ -6,7 +6,7 @@ module.exports = (isDebug) => {
 
 
 	winston.remove(winston.transports.Console);
 	winston.remove(winston.transports.Console);
 	winston.add(winston.transports.Console, {
 	winston.add(winston.transports.Console, {
-		level: (isDebug) ? 'info' : 'warn',
+		level: (isDebug) ? 'debug' : 'info',
 		prettyPrint: true,
 		prettyPrint: true,
 		colorize: true,
 		colorize: true,
 		silent: false,
 		silent: false,

+ 64 - 1
models/user.js

@@ -2,6 +2,7 @@
 
 
 const modb = require('mongoose'),
 const modb = require('mongoose'),
 			Promise = require('bluebird'),
 			Promise = require('bluebird'),
+			bcrypt = require('bcryptjs-then'),
 			_ = require('lodash');
 			_ = require('lodash');
 
 
 /**
 /**
@@ -12,13 +13,75 @@ const modb = require('mongoose'),
 var userSchema = modb.Schema({
 var userSchema = modb.Schema({
 
 
 	email: {
 	email: {
+		type: String,
+		required: true,
+		index: true
+	},
+
+	provider: {
 		type: String,
 		type: String,
 		required: true
 		required: true
-	}
+	},
+
+	providerId: {
+		type: String
+	},
+
+	password: {
+		type: String
+	},
+
+	name: {
+		type: String
+	},
+
+	rights: [{
+		type: String
+	}]
 
 
 },
 },
 {
 {
 	timestamps: {}
 	timestamps: {}
 });
 });
 
 
+userSchema.statics.processProfile = (profile) => {
+
+	let primaryEmail = '';
+	if(_.isArray(profile.emails)) {
+		let e = _.find(profile.emails, ['primary', true]);
+		primaryEmail = (e) ? e.value : _.first(profile.emails).value;
+	} else if(_.isString(profile.email) && profile.email.length > 5) {
+		primaryEmail = profile.email;
+	} else {
+		return Promise.reject(new Error('Invalid User Email'));
+	}
+	
+	return db.User.findOneAndUpdate({
+		email: primaryEmail,
+		provider: profile.provider
+	}, {
+		email: primaryEmail,
+		provider: profile.provider,
+		providerId: profile.id,
+		name: profile.displayName
+	}, {
+		new: true,
+		upsert: true
+	}).then((user) => {
+	  return (user) ? user : Promise.reject(new Error('User Upsert failed.'));
+	});
+
+};
+
+userSchema.statics.hashPassword = (rawPwd) => {
+	return bcrypt.hash(rawPwd);
+};
+
+userSchema.methods.validatePassword = function(rawPwd) {
+	let self = this;
+	return bcrypt.hash(rawPwd).then((pwd) => {
+		return (self.password === pwd) ? true : Promise.reject(new Error('Invalid Password'));
+	});
+};
+
 module.exports = modb.model('User', userSchema);
 module.exports = modb.model('User', userSchema);

+ 4 - 0
package.json

@@ -73,7 +73,11 @@
     "mongoose": "^4.6.3",
     "mongoose": "^4.6.3",
     "multer": "^1.2.0",
     "multer": "^1.2.0",
     "passport": "^0.3.2",
     "passport": "^0.3.2",
+    "passport-facebook": "^2.1.1",
+    "passport-google-oauth20": "^1.0.0",
     "passport-local": "^1.0.0",
     "passport-local": "^1.0.0",
+    "passport-windowslive": "^1.0.2",
+    "passport.socketio": "^3.6.2",
     "pug": "^2.0.0-beta6",
     "pug": "^2.0.0-beta6",
     "read-chunk": "^2.0.0",
     "read-chunk": "^2.0.0",
     "remove-markdown": "^0.1.0",
     "remove-markdown": "^0.1.0",

+ 27 - 8
server.js

@@ -46,6 +46,7 @@ const http = require('http');
 const i18next_backend = require('i18next-node-fs-backend');
 const i18next_backend = require('i18next-node-fs-backend');
 const i18next_mw = require('i18next-express-middleware');
 const i18next_mw = require('i18next-express-middleware');
 const passport = require('passport');
 const passport = require('passport');
+const passportSocketIo = require('passport.socketio');
 const path = require('path');
 const path = require('path');
 const session = require('express-session');
 const session = require('express-session');
 const sessionMongoStore = require('connect-mongo')(session);
 const sessionMongoStore = require('connect-mongo')(session);
@@ -81,15 +82,16 @@ app.use(express.static(path.join(ROOTPATH, 'assets')));
 // Session
 // Session
 // ----------------------------------------
 // ----------------------------------------
 
 
-var strategy = require('./libs/auth')(passport, appconfig);
+const strategies = require('./libs/auth')(passport, appconfig);
+var sessionStore = new sessionMongoStore({
+  mongooseConnection: db.connection,
+  touchAfter: 15
+});
 
 
 app.use(cookieParser());
 app.use(cookieParser());
 app.use(session({
 app.use(session({
   name: 'requarkswiki.sid',
   name: 'requarkswiki.sid',
-  store: new sessionMongoStore({
-    mongooseConnection: db.connection,
-    touchAfter: 15
-  }),
+  store: sessionStore,
   secret: appconfig.sessionSecret,
   secret: appconfig.sessionSecret,
   resave: false,
   resave: false,
   saveUninitialized: false
   saveUninitialized: false
@@ -144,9 +146,9 @@ app.use(mw.flash);
 
 
 app.use('/', ctrl.auth);
 app.use('/', ctrl.auth);
 
 
-app.use('/uploads', ctrl.uploads);
+app.use('/uploads', mw.auth, ctrl.uploads);
 app.use('/admin', mw.auth, ctrl.admin);
 app.use('/admin', mw.auth, ctrl.admin);
-app.use('/', ctrl.pages);
+app.use('/', mw.auth, ctrl.pages);
 
 
 // ----------------------------------------
 // ----------------------------------------
 // Error handling
 // Error handling
@@ -202,9 +204,26 @@ server.on('listening', () => {
 });
 });
 
 
 // ----------------------------------------
 // ----------------------------------------
-// WebSocket handlers
+// WebSocket
 // ----------------------------------------
 // ----------------------------------------
 
 
+io.use(passportSocketIo.authorize({
+  key: 'requarkswiki.sid',
+  store: sessionStore,
+  secret: appconfig.sessionSecret,
+  passport,
+  cookieParser,
+  success: (data, accept) => {
+    accept();
+  },
+  fail: (data, message, error, accept) => {
+    if(error) {
+      throw new Error(message);
+    }
+    return accept(new Error(message));
+  }
+}));
+
 io.on('connection', ctrl.ws);
 io.on('connection', ctrl.ws);
 
 
 // ----------------------------------------
 // ----------------------------------------

+ 23 - 4
views/auth/login.pug

@@ -27,14 +27,33 @@ html
 
 
 	body
 	body
 		#bg
 		#bg
-			each bg in [1, 2, 3]
+			each bg in _.sampleSize([1, 2, 3],3)
 				div(style="background-image:url(/images/bg_" + bg + ".jpg);")
 				div(style="background-image:url(/images/bg_" + bg + ".jpg);")
 		#root
 		#root
 			h1= appconfig.title
 			h1= appconfig.title
 			h2 Login required
 			h2 Login required
-			input#login-user(type='text', placeholder='Email address')
-			input#login-pass(type='password', placeholder='Password')
-			button Log In
+			if appconfig.auth.local.enabled
+				input#login-user(type='text', placeholder='Email address')
+				input#login-pass(type='password', placeholder='Password')
+				button Log In
+			if appconfig.authStrategies.socialEnabled
+				#social
+					if appconfig.auth.local.enabled
+						span Or, log in using...
+					else
+						span Log in using...
+					if appconfig.auth.microsoft && appconfig.auth.microsoft.enabled
+						button.ms(onclick="window.location.assign('/login/ms')")
+							i.fa.fa-windows
+							span Microsoft Account
+					if appconfig.auth.google && appconfig.auth.google.enabled
+						button.google(onclick="window.location.assign('/login/google')")
+							i.fa.fa-google
+							span Google ID
+					if appconfig.auth.facebook && appconfig.auth.facebook.enabled
+						button.facebook(onclick="window.location.assign('/login/facebook')")
+							i.fa.fa-facebook
+							span Facebook
 		#copyright
 		#copyright
 			= t('footer.poweredby') + ' '
 			= t('footer.poweredby') + ' '
 			a.icon(href='https://github.com/Requarks/wiki')
 			a.icon(href='https://github.com/Requarks/wiki')

Some files were not shown because too many files changed in this diff