@@ -1,15 +0,0 @@ | |||
const mongoose = require('mongoose'); | |||
//const Schema = mongoose.Schema, | |||
//ObjectId = Schema.ObjectId; | |||
const MessageSchema = mongoose.Schema({ | |||
subject: { type: String, required: true }, | |||
message: { type: String, required: true }, | |||
user: { type: String, required: true }, | |||
tag: [{type: String }], | |||
//createtime: { type: Date, default: Date.now }, | |||
}); | |||
MessageSchema.index({tag:'text'}); | |||
module.exports = mongoose.model('Message', MessageSchema); |
@@ -1,3 +0,0 @@ | |||
module.exports = { | |||
url: 'mongodb://localhost:27017/mydb' | |||
} |
@@ -58,8 +58,8 @@ | |||
<!-- NOTE: Load JQuery, Vue.js, VueRouter, Vuetify --> | |||
<script src="lib/jquery-3.3.1.min.js"></script> | |||
<script src="lib/vue.js"></script> | |||
<script src="lib/vue-router.js"></script> | |||
<script src="lib/vue-2.6.10.js"></script> | |||
<script src="lib/vue-router-3.0.1.js"></script> | |||
<!-- Buefy/Vuetify --> | |||
<script src="lib/buefy-0.7.5.js"></script> | |||
@@ -84,6 +84,7 @@ | |||
<script src="routes/profil.js"></script> | |||
<script src="routes/msgCard.js"></script> | |||
<script src="routes/messageData.js"></script> | |||
<script src="routes/tagData.js"></script> | |||
<script src="routes/profilCard.js"></script> | |||
<script src="routes/profilData.js"></script> | |||
<script src="routes/login.js"></script> | |||
@@ -97,12 +98,36 @@ | |||
<div class="om-header-container"> | |||
<div class="om-header"> | |||
<a class="logo-img" href="index.html"><img src="img/app_icon.png" width=45px height=45px></a> | |||
<form class="om-searchbar"> | |||
<form class="om-searchbar" @submit.prevent="search()"> | |||
<b-field> | |||
<b-input placeholder="Suche..." | |||
type="search" | |||
icon="magnify"> | |||
</b-input> | |||
<b-autocomplete | |||
rounded | |||
v-model="searchtext" | |||
@keydown.native.enter="search" | |||
:data="filteredDataArray" | |||
placeholder="suche..." | |||
icon="magnify" | |||
@select="option => selected = option"> | |||
</b-autocomplete> | |||
<!--<b-input | |||
type="search" | |||
v-model="searchtext" | |||
icon="magnify" | |||
placeholder="Suche..."> | |||
</b-input>--> | |||
<!--<b-taginput | |||
id="search-text" | |||
type="search" | |||
v-model="selected" | |||
:data=taglist | |||
autocomplete | |||
allow-new:true | |||
icon="magnify" | |||
placeholder="Suche.." | |||
@typing="getFilteredTags" | |||
@input="saveTagsToArray"> | |||
</b-taginput>--> | |||
</b-field> | |||
</form> | |||
</div> | |||
@@ -161,6 +186,12 @@ | |||
</div> | |||
<script> | |||
var tagArray = ["lorem"]; | |||
const dat=[ | |||
'th', | |||
'efi', | |||
'wichtig', | |||
]; | |||
const routes = [ | |||
{ path: "/", component: HomeRouter }, | |||
@@ -179,11 +210,127 @@ | |||
var app = new Vue({ | |||
router, | |||
el: '#api', | |||
data: { | |||
data: function() { | |||
return{ | |||
searchtext: "", | |||
selected: [], | |||
taglist: dat, | |||
}; | |||
}, | |||
computed:{ | |||
filteredDataArray() { | |||
return this.taglist.filter((option) => { | |||
return option | |||
.toString() | |||
.toLowerCase() | |||
.indexOf(this.searchtext.toLowerCase()) >= 0 | |||
}) | |||
} | |||
}, | |||
methods: { | |||
} | |||
search: function() { | |||
console.log("Searchtext: " +this.searchtext.replace(/#/g,'')); | |||
$.ajax({ | |||
url: "api/msg/search/" + this.searchtext.replace(/#/g, ''), | |||
method: "GET" | |||
}).done(jd => { | |||
// NICHT SO wg. Vue: _messagelist = jd; | |||
_messagelist.splice(0, _messagelist.length); | |||
_messagelist.push.apply(_messagelist, jd); | |||
//console.log("jd: "+jd); | |||
for (var e in jd) { | |||
if (!_messages[jd[e]]) { | |||
get_insert_message(jd[e]); | |||
} | |||
} | |||
}).fail(function(e, f, g) { | |||
console.log("searching: err: " + e + f + g); | |||
}) | |||
}, | |||
getFilteredTags(text) { | |||
this.taglist = dat.filter((option) => { | |||
return option | |||
.toString() | |||
.toLowerCase() | |||
.indexOf(text.toLowerCase()) >= 0 | |||
}) | |||
this.search(); | |||
}, | |||
saveTagsToArray: function() { | |||
tagArray = this.selected; | |||
console.info(tagArray); | |||
}, | |||
list_tags: function () { | |||
$.ajax({url: "api/tag/ids",method: "GET"}) | |||
.done(jd => { | |||
// NICHT SO wg. Vue: _messagelist = jd; | |||
_taglist.splice(0, _taglist.length); | |||
_taglist.push.apply(_taglist, jd); | |||
console.log("tag: jd: " + jd); | |||
for (var e in jd) { | |||
if (!_tags[jd[e]]) { | |||
get_insert_tag(jd[e]); | |||
} | |||
} | |||
}).fail(function (e, f, g) { | |||
console.log("err: " + e + f + g); | |||
}); | |||
} | |||
}, | |||
mounted: function () { | |||
this.search(); | |||
this.list_tags(); | |||
}, | |||
}); | |||
function get_insert_tag(id){ | |||
$.ajax({ url: "api/tag/"+id, method: "GET" }).done(function (tag) { | |||
dat.push("#"+tag.name); | |||
console.log("it worked!"); | |||
}).fail(function (e, f, g) { | |||
console.log("cannot load " + id + ".json: " + e + f + g); | |||
}) | |||
} | |||
const search_data = []; | |||
/* | |||
export.default { | |||
data: function () { | |||
return { | |||
selected: [], | |||
taglist: data, | |||
}; | |||
}, | |||
computed: { | |||
filteredTagArray() { | |||
this.taglist = data.filter((option) => { | |||
return option | |||
.toString() | |||
.toLowerCase() | |||
.indexOf(this.name.toLowerCase()) >= 0 | |||
}) | |||
}, | |||
}, | |||
methods: { | |||
evaluate(text) { | |||
this.taglist = data.filter((option) => { | |||
return option | |||
.toString() | |||
.toLowerCase() | |||
.indexOf(text.toLowerCase()) >= 0 | |||
}) | |||
}, | |||
}, | |||
mounted: function () { | |||
if ($(this).bootstrapMaterialDesign) | |||
$(this).bootstrapMaterialDesign(); | |||
}, | |||
}; | |||
*/ | |||
</script> | |||
<!-- CDN_Vue.js minified lib --> |
@@ -4,7 +4,7 @@ | |||
// Wait until page loaded | |||
window.addEventListener('load', () => { | |||
if ('serviceWorker' in navigator) { | |||
/*if ('serviceWorker' in navigator) { | |||
// NOTE: ServiceWorker Registration | |||
return navigator.serviceWorker.register('serviceWorker.js', { | |||
scope: '/' | |||
@@ -16,7 +16,7 @@ window.addEventListener('load', () => { | |||
} else { | |||
console.log('[ServiceWorker] are not supported.'); | |||
return; | |||
} | |||
}*/ | |||
}); | |||
// NOTE: Set Bootstrap materialdesign |
@@ -1,7 +1,8 @@ | |||
var tagArray = []; | |||
const data=[ | |||
'#th', | |||
'#efi', | |||
'#wichtig', | |||
'th', | |||
'efi', | |||
'wichtig', | |||
]; | |||
const CreateMsgRouter = { | |||
template: ` | |||
@@ -18,13 +19,15 @@ const CreateMsgRouter = { | |||
<b-field label="Tags"> | |||
<b-taginput | |||
id="tag" | |||
v-model="selected" | |||
:data=items | |||
:data=taglist | |||
autocomplete | |||
allow-new:false | |||
icon="label" | |||
placeholder="#" | |||
@typing="getFilteredTags" id="tag"> | |||
@typing="getFilteredTags" | |||
@input="saveTagsToArray"> | |||
</b-taginput> | |||
</b-field> | |||
@@ -33,38 +36,41 @@ const CreateMsgRouter = { | |||
</b-field> | |||
<b-button @click="$router.go(-1)">ABBRECHEN</b-button> | |||
<b-button type="is-primary" @click="$router.push('/home')">SENDEN</b-button> | |||
<b-button type="is-primary" @click="createMsg">SENDEN</b-button> | |||
</div> | |||
`, | |||
data: function () { | |||
return { | |||
selected: [], | |||
items: data | |||
taglist: data, | |||
}; | |||
}, | |||
methods: { | |||
saveTagsToArray: function() { | |||
tagArray = this.selected; | |||
console.info(tagArray); | |||
}, | |||
createMsg: function () { | |||
var subject = $("#subject").val(); | |||
var message = $("#message").val(); | |||
var tag = $("#tag").val(); | |||
var user = $("#user").val(); | |||
console.log("Message Created: " + tag + " " + message + " " + user); | |||
var _subject = $("#subject").val(); | |||
var _message = $("#message").val(); | |||
var _tag = tagArray; | |||
var _user = $("#user").val(); | |||
//console.log("Message Created: " + _tag + " " + _message + " " + _user); | |||
$.ajax({ | |||
url: "api/createMsg", | |||
url: "api/msg", | |||
data: { | |||
sub: subject, | |||
mess: message, | |||
use: user, | |||
ta: tag | |||
subject: _subject, | |||
message: _message, | |||
user: _user, | |||
tag: _tag | |||
}, | |||
method: "POST" | |||
}).done(have_result).fail(have_error); | |||
function have_result(res) { | |||
console.log(res); | |||
//console.log(res); | |||
router.push('/home') | |||
} | |||
function have_error(err) { | |||
@@ -73,16 +79,42 @@ const CreateMsgRouter = { | |||
} | |||
}, | |||
getFilteredTags(text) { | |||
this.items = data.filter((option) => { | |||
this.taglist = data.filter((option) => { | |||
return option | |||
.toString() | |||
.toLowerCase() | |||
.indexOf(text.toLowerCase()) >= 0 | |||
}) | |||
}, | |||
}, | |||
list_tags: function () { | |||
$.ajax({url: "api/tag/ids",method: "GET"}) | |||
.done(jd => { | |||
// NICHT SO wg. Vue: _messagelist = jd; | |||
_taglist.splice(0, _taglist.length); | |||
_taglist.push.apply(_taglist, jd); | |||
console.log("tag: jd: " + jd); | |||
for (var e in jd) { | |||
if (!_tags[jd[e]]) { | |||
get_insert_tag(jd[e]); | |||
} | |||
} | |||
}).fail(function (e, f, g) { | |||
console.log("err: " + e + f + g); | |||
}); | |||
} | |||
}, | |||
mounted: function () { | |||
this.list_tags(); | |||
if ($(this).bootstrapMaterialDesign) | |||
$(this).bootstrapMaterialDesign(); | |||
}, | |||
}; | |||
function get_insert_tag(id){ | |||
$.ajax({ url: "api/tag/"+id, method: "GET" }).done(function (tag) { | |||
data.push("#" + tag.name); | |||
console.log("Array:"+this.data); | |||
}).fail(function (e, f, g) { | |||
console.log("cannot load " + id + ".json: " + e + f + g); | |||
}) | |||
} |
@@ -24,12 +24,12 @@ const HomeRouter = { | |||
}); | |||
},*/ | |||
list_messages: function () { | |||
$.ajax({url: "api/ids", method: "GET"}) | |||
$.ajax({url: "api/msg/ids", method: "GET"}) | |||
.done(jd => { | |||
// NICHT SO wg. Vue: _messagelist = jd; | |||
_messagelist.splice(0, _messagelist.length); | |||
_messagelist.push.apply(_messagelist, jd); | |||
console.log("jd: "+jd); | |||
//console.log("jd: "+jd); | |||
for (var e in jd) { | |||
if (!_messages[jd[e]]) { | |||
get_insert_message(jd[e]); | |||
@@ -49,7 +49,7 @@ const HomeRouter = { | |||
} | |||
}*/ | |||
}).fail(function (e, f, g) { | |||
console.log("err: " + e + f + g); | |||
console.log("list_msg: err: " + e + f + g); | |||
}); | |||
} | |||
}, | |||
@@ -61,7 +61,7 @@ const HomeRouter = { | |||
}; | |||
function get_insert_message(id) { | |||
$.ajax({ url: "api/msg/"+id, method: "GET" }).done(function (msg) { | |||
$.ajax({ url: "api/msg/id/"+id, method: "GET" }).done(function (msg) { | |||
Vue.set(_messages, id, msg); | |||
}).fail(function (e, f, g) { | |||
console.log("cannot load " + id + ".json: " + e + f + g); |
@@ -5,7 +5,8 @@ Vue.component('MsgCard', { | |||
<img src="favicon.ico" width=20px height=20px> | |||
</h6> | |||
{{ msg.message }}<br><br> | |||
<a href="#">{{ msg.tag }}</a></p> | |||
<a v-for="tag in msg.tag" href="#">#{{ tag }} </a> | |||
</p> | |||
<div class="om-card-footer"> <div class="om-user-line"> | |||
<i class="material-icons">account_circle</i> | |||
Erstellt von {{ msg.user }}</div> |
@@ -0,0 +1,2 @@ | |||
_taglist = []; | |||
_tags = []; |
@@ -1,230 +1,176 @@ | |||
// Original file created by Prof.Dr. Matthias Hopf | |||
/** | |||
* Express based http & https server | |||
* | |||
* Requires express >= 4 | |||
*/ | |||
var common = require('./server/common'), | |||
authorize = require('./server/authorization'), | |||
dbs = require('./server/dbs'); | |||
/* | |||
var common = require ('./server/common'), | |||
authorize = require ('./server/authorization'), | |||
dbs = require ('./server/dbs'), | |||
files = require ('./server/files'); | |||
*/ | |||
var fs = require ('fs'), | |||
http = require ('http'), | |||
https = require ('https'), | |||
express = require ('express'), | |||
session = require ('express-session'), // session management | |||
morgan = require ('morgan'), // logger | |||
//serveFavicon = require ('serve-favicon'), | |||
bodyParser = require ('body-parser'); | |||
//MongoStore = require ('connect-mongo')(session); // uss mongodb as session storage | |||
var Message = require('./message.model.js'); | |||
const fs = common.fs, // file sync, read certificates | |||
http = common.http, // http handler | |||
https = require('https'), // https handler | |||
express = require('express'), // node server framework | |||
session = require('express-session'), // session management (security) | |||
morgan = require('morgan'), // logger | |||
mong = common.mongoose, // mongoose | |||
// serveFavicon = require('serve-favicon'), // provide favicon | |||
bodyParser = require('body-parser'), // post request bodyparser | |||
MongoStore = require('connect-mongo')(session); // use mongodb as session storage | |||
var app = express(); | |||
var http_port=8013; | |||
https_port=8889; | |||
/* | |||
* Init | |||
*/ | |||
/*ll | |||
common .init (); | |||
authorize.init (common); | |||
common .init(); | |||
authorize .init(common); | |||
dbs .init (common); | |||
files .init (common); | |||
*/ | |||
//files .init (common); | |||
mong.Promise = global.Promise; | |||
// Security | |||
app.disable ('x-powered-by'); // TODO: Disable Header information: Powerd by Express -> Information disclosure | |||
/* | |||
* Security | |||
* | |||
* TODO: Install helmet | |||
* https://expressjs.com/de/advanced/best-practice-security.html | |||
* | |||
* (Disable Header information: Powerd by Express) | |||
* -> Information disclosure | |||
*/ | |||
app.disable('x-powered-by'); | |||
/* | |||
* Route Control | |||
*/ | |||
// Logger | |||
app.use (morgan ('dev')); | |||
//app.use(express.logger ( { format: 'default', stream: output_stream } )); | |||
// Fastpaths | |||
//app.use (serveFavicon (__dirname + '/public/favicon.ico')); | |||
// Session Management | |||
app.use (session({ | |||
secret: 'adluhohks', | |||
resave: false, | |||
saveUninitialized: false, | |||
cookie: { | |||
maxAge: 30*24*3600*1000, // TODO: ttl for session as well (Store) | |||
secure: false, // true for https only | |||
}, | |||
name: 'om.sid', | |||
//store: new MongoStore ({mongooseConnection: dbs.mongoose.connection, ttl: 30*24*3600}), // mongoose + connect-mongo | |||
//store: new MemoryStore ({checkPeriod: 24*3600*1000}), // memorystore | |||
app.set('trust proxy', 1) // trust first proxy, neccessary for cookie secure: true flag | |||
app.use(session({ | |||
secret: 'ahhgylhuvh', // caesar(3) 2 letter surname | |||
resave: false, | |||
saveUninitialized: false, | |||
cookie: { | |||
maxAge: 30 * 24 * 3600 * 1000, // TODO: ttl for session as well (Store) | |||
secure: true, // true for https only (since our app works only with https) | |||
}, | |||
name: 'om.sid', | |||
store: new MongoStore({ | |||
mongooseConnection: mong.connection, | |||
ttl: 30 * 24 * 3600 | |||
}), // mongoose + connect-mongo | |||
//store: new MemoryStore ({checkPeriod: 24*3600*1000}), // memorystore | |||
})); | |||
// Args | |||
app.use (bodyParser.json()); | |||
app.use (bodyParser.urlencoded({extended: true})); | |||
// API | |||
//var api_routes = express.Router(); // express app-object routing | |||
//app.use ('/api', api_routes); | |||
app.use (function (req, res, done) { | |||
console.log(req.url); | |||
done(); | |||
}); | |||
//global.__basedir = __dirname; | |||
// Static Files | |||
app.use(express.static(__dirname + '/public')); // Allow server access to 'public' folder | |||
//app.use(express.static('resources')); | |||
// Configuring the database | |||
var dbConfig = require('./mongodb.config.js'); | |||
var mongoose = require('mongoose'); | |||
mongoose.Promise = global.Promise; | |||
// Connecting to the database | |||
//mongoose.connect(`mongodb://${server}/${dbConfig.url}`) | |||
mongoose.connect(dbConfig.url, {useNewUrlParser: true}).then(() => { | |||
console.log("Successfully connected to MongoDB."); | |||
}).catch(err => { | |||
console.log('Could not connect to MongoDB.'); | |||
process.exit(); | |||
}); | |||
//require('./app/routes/message.route.js')(app); | |||
app.get ('/api/ids', function (req, res) { | |||
Message.find({},{id: true}) .exec () .then(results => { | |||
//selects id from message: | |||
var parsed = []; | |||
for (var i in results) { | |||
parsed.push (results[i].id); | |||
} | |||
//var parsed = results.map (x => x._id); | |||
res.send(parsed); | |||
} ) | |||
.catch(err => { | |||
console.log (err); | |||
res .status(500) .json (err); | |||
}); | |||
}); | |||
// TODO Favicon for Desktop | |||
//app.use (serveFavicon (__dirname + '/public/favicon.ico')); | |||
app.get ("/api/msg/:id", function (req, res) { | |||
Message.findOne ({_id: req.params.id}) .exec (function (err, results){ | |||
if (err) { | |||
console.log (err); | |||
res .status(404) .json (err); | |||
} else { | |||
console.log(JSON.stringify(results)); | |||
res.json(results); | |||
} | |||
}); | |||
// Minimal Logging | |||
//app.use (morgan ('dev')); | |||
// Advanced Logging | |||
morgan.token('user', function (req, res) { | |||
return (req.session && req.session.user) || '-'; | |||
}); | |||
/*app.get ("/api/msg/search/:phrase", function (req, res) { | |||
Message.find ({$text: {$search: req.params.phrase}) .then (function (err, results){ | |||
if (err) { | |||
console.log (err); | |||
res .status(404) .json (err); | |||
} else { | |||
console.log(JSON.stringify(results)); | |||
res.json(results); | |||
} | |||
}); | |||
morgan.token('userColored', function (req, res) { | |||
var color = 0; | |||
if (req.session && req.session.roles) | |||
color = req.session.roles.admin ? 31 // red | |||
: req.session.roles.user ? 34 // blue | |||
: 0; // no color | |||
return '\x1b[' + color + 'm' + ((req.session && req.session.user) || '-') + '\x1b[0m'; | |||
}); | |||
*/ | |||
/*function makeid() { | |||
var text = ""; | |||
var possible = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"; | |||
for (var i = 0; i < 5; i++) | |||
text += possible.charAt(Math.floor(Math.random() * possible.length)); | |||
return text; | |||
}*/ | |||
app.post("/api/createMsg", function(req, res){ | |||
//x = mongoose.Types.ObjectId(); | |||
//y = x.toString(); | |||
//var z = makeid(); | |||
console.log("SUbject: "+JSON.stringify(req.body)); | |||
var message = new Message( {subject: req.body.sub, message: req.body.mess, user: req.body.use, tag: req.body.ta } ); | |||
message.save(function(err,result){ | |||
if(err){ | |||
return res .status(401) .send(err.message); | |||
}else{ | |||
res.json({message: "Message created!!"}); | |||
} | |||
}); | |||
morgan.token('statusColored', function (req, res) { | |||
var color = res.statusCode >= 500 ? 31 // red | |||
: res.statusCode >= 400 ? 33 // yellow | |||
: res.statusCode >= 300 ? 36 // cyan | |||
: res.statusCode >= 200 ? 32 // green | |||
: 0; // no color | |||
return '\x1b[' + color + 'm' + (res.headersSent ? res.statusCode : '-') + '\x1b[0m'; | |||
}); | |||
app.use(morgan(':date[iso] :statusColored :method :url :userColored :response-time ms :res[content-length]')); | |||
// BodyParser | |||
// Returns middleware that only parses json bodies. | |||
// (https://www.npmjs.com/package/body-parser#bodyparserjsonoptions) | |||
app.use(bodyParser.json()); | |||
// Returns middleware that only parses urlencoded bodies | |||
// with qs library (https://www.npmjs.com/package/qs#readme) | |||
app.use(bodyParser.urlencoded({ | |||
extended: true | |||
})); | |||
// Other stuff is NOT authorized unless logged in | |||
//app.use (authorize.genCheckAuthorized ('user')); | |||
// API | |||
var api_routes = express.Router(); // express app-object routing | |||
app.use('/api', api_routes); | |||
// Uploaded files | |||
//app.use ('/uploads', expr ess.static(__dirname + '/uploads')); | |||
// Static Files - Allow access to 'public' folder | |||
app.use(express.static(__dirname + '/public')); | |||
// Other stuff is NOT authorized unless logged in | |||
//app.use (authorize.genCheckAuthorized ('user')); | |||
// Uploaded files | |||
//app.use ('/uploads', express.static(__dirname + '/uploads')); | |||
// Errors | |||
// No error so far? Then it's a 404! | |||
//app.use (function (req, res, next) { next (common.genError (404, req.url)); }); | |||
//app.use (routes.errorHandler (true)); /* true: show stack traces */ // TODO: Error Handler | |||
app.use(function (req, res, next) { | |||
next(common.genError(404, req.url)); | |||
}); | |||
//app.use (routes.errorHandler (true)); /* true: show stack traces */ | |||
/* | |||
* API | |||
*/ | |||
/* | |||
// API allowed for all | |||
api_routes.post ('/login', authorize.login); // /api/login | |||
api_routes.post('/login', authorize.login); | |||
// Validate all other API calls | |||
api_routes.use (authorize.genCheckAuthorized ('user')); | |||
api_routes.post ('/logout', authorize.logout); | |||
function addRoutes (r) { | |||
for (var e in r.routes) { | |||
var params = r.routes[e].params ? "/" + r.routes[e].params : ""; | |||
console.log ("Adding routes for /" + e + params + ":" + | |||
(r.routes[e].get ? " get":" ") + (r.routes[e].post ? " post":" ") + | |||
(r.routes[e].put ? " put":" ") + (r.routes[e].delete ? " delete":" ")); | |||
if (r.routes[e].get) | |||
api_routes.get ('/' + e + params, r.routes[e].get); | |||
if (r.routes[e].post) | |||
api_routes.post ('/' + e + params, r.routes[e].post); | |||
if (r.routes[e].put) | |||
api_routes.put ('/' + e + params, r.routes[e].put); | |||
if (r.routes[e].delete) | |||
api_routes.delete ('/' + e + params, r.routes[e].delete); | |||
} | |||
//api_routes.use(authorize.genCheckAuthorized('user')); | |||
api_routes.post('/logout', authorize.logout); | |||
// Add API routes | |||
function addRoutes(r) { | |||
for (var e in r.routes) { | |||
var route = '/' + e + (r.routes[e].params ? "/" + r.routes[e].params : ""); | |||
var log = "Adding routes for " + route + ":"; | |||
/* | |||
var auth = r.routes[e].auth || r.auth; | |||
if (auth) { | |||
log += " [auth]"; | |||
api_routes.use (route, function (req, res, next) { | |||
if (! auth(req)) | |||
return next (common.genError (403, "Unauthorized")); | |||
next (); | |||
}); | |||
} | |||
*/ | |||
/* | |||
var role = r.routes[e].role || r.role; | |||
if (role) { | |||
log += " [role:"+role+"]"; | |||
api_routes.use (route, authorize.genCheckAuthorized (role)); | |||
} | |||
*/ | |||
const methods = ["get", "post", "put", "delete"]; | |||
for (var m in methods) { | |||
if (r.routes[e][methods[m]]) { | |||
log += " " + methods[m]; | |||
api_routes[methods[m]](route, r.routes[e][methods[m]]); | |||
} | |||
} | |||
console.log(log); | |||
} | |||
} | |||
addRoutes (dbs); | |||
addRoutes (files); | |||
*/ | |||
addRoutes(dbs); | |||
/* | |||
* Servers | |||
*/ | |||
http.createServer (app) .listen (http_port, function () { | |||
console.log ("Express http server listening on port " + http_port); | |||
http.createServer(app).listen(common.config.httpPort, function () { | |||
console.log("Express http server listening on port " + common.config.httpPort); | |||
}); | |||
/* | |||
@@ -238,39 +184,37 @@ http.createServer (app) .listen (http_port, function () { | |||
* openssl x509 -req -in certrequest.csr -signkey privatekey.pem -out certificate.pem | |||
* rm certrequest.csr | |||
*/ | |||
var options; | |||
try { | |||
try { | |||
// In case it's a real certificate: add CA chain cersts (TODO: use array if required) | |||
var ca = fs.readFileSync ('keys/ca_cert.pem'); | |||
} catch (e) { | |||
ca = undefined; | |||
console.log ("Note: Can't read CA bundle: "+e); | |||
} | |||
if (ca != null) { | |||
options = { | |||
key: fs.readFileSync ('keys/omkey.pem'), | |||
cert: fs.readFileSync ('keys/certificate.pem'), | |||
ca: ca | |||
}; | |||
https.createServer (options, app) .listen (https_port, function () { | |||
console.log ("Express https server listening on port " + https_port); | |||
}); | |||
if (common.config.httpsPort) { | |||
var options; | |||
try { | |||
try { | |||
// In case it's a real certificate: add CA chain cersts (TODO: use array if required) | |||
var ca = fs.readFileSync('keys/ca_cert.pem'); | |||
} catch (e) { | |||
ca = undefined; | |||
console.log("Note: Can't read CA bundle: " + e); | |||
} | |||
if (ca != undefined) { | |||
options = { | |||
key: fs.readFileSync('keys/omkey.pem'), | |||
cert: fs.readFileSync('keys/certificate.pem'), | |||
ca: ca | |||
}; | |||
https.createServer(options, app).listen(common.config.httpsPort, function () { | |||
console.log("Express https server listening on port " + common.config.httpsPort); | |||
}); | |||
} | |||
} catch (e) { | |||
console.log("Note: Can't read SSL keys/certs: " + e + "\nDisabling https server"); | |||
} | |||
} catch (e) { | |||
console.log ("Note: Can't read SSL keys/certs: "+e+"\nDisabling https server"); | |||
} else { | |||
console.log("Note: https server disabled by config"); | |||
} | |||
/* | |||
* Uncaught Exceptions | |||
*/ | |||
process.on ("uncaughtException", function (err) { | |||
console.error ("*** Uncaught Exception:"); | |||
console.error (err.stack); | |||
process.on("uncaughtException", function (err) { | |||
console.error("*** Uncaught Exception:"); | |||
console.error(err.stack); | |||
}); | |||
@@ -0,0 +1,118 @@ | |||
// Original file created by Prof.Dr. Matthias Hopf | |||
/* | |||
* Authorization | |||
*/ | |||
var common, User; | |||
const ldap = require ('./ldap_ohm'), | |||
crypto = require ("../server/crypto"); | |||
// deactivated is not used yet | |||
const serverVisibleSession = { user: true, name: true, type: true, mail: true, roles: true, deactivated: true, host: true }; | |||
const clientVisibleSession = { user: true, name: true, type: true, mail: true, roles: true }; | |||
// Fill in session object | |||
function fillSession (req, user, roles, cb) { | |||
if (req.session === undefined) | |||
next (common.genError (500, "Error")); | |||
req.session.regenerate (function (err) { | |||
if (user !== undefined && ! err) { | |||
common.shallowCopy (user, serverVisibleSession, {roles: true}, req.session); | |||
if (user._id) { | |||
req.session.user = user._id; | |||
} | |||
req.session.roles = roles; | |||
} | |||
return cb (err); | |||
}); | |||
} | |||
const authorization = { | |||
// Generate Error object suitible for throwing or next()ing | |||
genCheckAuthorized: function (group) { | |||
return function (req, res, next) { | |||
if (req.session === undefined || req.session.user === undefined || | |||
req.session.roles === undefined) | |||
return next (common.genError (403, "Unauthorized")); | |||
if (req.session.roles[group] === undefined) | |||
return next (common.genError (403, "Unauthorized")); | |||
next (); | |||
} | |||
}, | |||
// Login route: requires .user and .pwd params | |||
login: function (req, res, next) { | |||
var user = req.body.user || ''; | |||
var pwd = req.body.pwd || ''; | |||
// Helper: Return valid session Object | |||
function returnSession () { | |||
// Only export client visible parts of session object | |||
var copy = common.shallowCopy (req.session, clientVisibleSession); | |||
return res.json (copy); | |||
} | |||
// Helper: Return error | |||
function returnError () { | |||
fillSession (req, undefined, undefined, function (err) { | |||
next (common.genError (401, "Unauthorized")); | |||
}); | |||
} | |||
// TODO Auth: validate session ID | |||
// Check whether to just validate current session ID | |||
if (user === '' && pwd === '') { | |||
console.log ("auth revalidate: " + req.session.user); | |||
if (req.session.user === undefined) | |||
return returnError(); | |||
return returnSession (); | |||
} | |||
/* | |||
// check local database, then ldap | |||
User.findById (req.body.user) .exec (function (err, entry) { | |||
// If there is a local user AND it has a password associated, test against this, and only this | |||
if (entry != null && entry.pwd) { | |||
if (crypto.checkLocalAuth (entry, req.body.pwd)) { | |||
return fillSession (req, entry, common.arrayToHash(entry.roles), returnSession); | |||
} | |||
return returnError (); | |||
} | |||
// check ldap | |||
ldap.authorize (user.toLowerCase(), pwd, function (found) { | |||
console.log ("ldap authorize " + user + " returns " + JSON.stringify (found)); | |||
// No ldap entry either -> unauthorized | |||
if (found == null) { | |||
return returnError (); | |||
} | |||
// If there is an entry w/o password, use it for roles etc. | |||
if (entry) { | |||
if (! entry.name || entry.name === "") | |||
entry.name = found.name; | |||
if (! entry.mail || entry.mail === "") | |||
entry.mail = found.mail; | |||
if (! entry.type || entry.type === "") | |||
entry.type = found.type; | |||
if (! entry.orclgender || entry.orclgender === "") | |||
entry.orclgender = found.orclgender; | |||
return fillSession (req, entry, entry.roles.length > 0 ? common.arrayToHash(entry.roles) : {user:true}, returnSession); | |||
} | |||
// Otherwise create standard user entry | |||
return fillSession (req, found, {user:true}, returnSession); | |||
}); | |||
});*/ | |||
}, | |||
logout: function (req, res, next) { | |||
fillSession (req, undefined, undefined, function (err) { | |||
return res.json ({}); | |||
}); | |||
}, | |||
init: function (_common) { | |||
common = _common; | |||
ldap.init (_common); | |||
//User = require('../database/user.model.js');; | |||
}, | |||
}; | |||
module.exports = authorization; |
@@ -0,0 +1,80 @@ | |||
// Original file created by Prof.Dr. Matthias Hopf | |||
/* | |||
* Common functions and imports | |||
*/ | |||
var common = { | |||
fs: require('fs'), // file sync | |||
http: require('http'), | |||
mongoose: require('mongoose'), // Needed for db connection. | |||
//util: require('util'), // Why is it needed here? | |||
//fork: require('child_process') .fork, // What does that? | |||
// Generate Error object suitible for throwing or next()ing | |||
// For a better exception handling | |||
genError: function (code, message) { | |||
var err = new Error (common.http.STATUS_CODES[code] + (message != undefined && message != "" ? ": "+message : "")); | |||
err.status = code; | |||
// to generally disable stack traces for these manually created error Objects: | |||
delete err.stack; | |||
return err; | |||
}, | |||
// Generate deep copy | |||
// Only include properties incl (all if undefined), strip properties excl (associative arrays) | |||
deepCopy: function (inp, incl, excl) { | |||
// For now, JSON is considered fastest / easiest | |||
var obj = JSON.parse (JSON.stringify (inp)); | |||
if (incl) { | |||
for (var k in obj) { | |||
if (incl[k] === undefined) | |||
delete obj[k]; | |||
} | |||
} | |||
if (excl) { | |||
for (var k in excl) { | |||
delete obj[k]; | |||
} | |||
} | |||
return obj; | |||
}, | |||
// Create shallow (1 level) copy of object, use obj if already present (merge) | |||
// Only include properties incl (all if undefined), strip properties excl (associative arrays) | |||
shallowCopy: function (inp, incl, excl, obj) { | |||
var keys = inp; | |||
if (obj === undefined) | |||
obj = {}; | |||
if (typeof inp == "array") | |||
obj = []; | |||
if (incl !== undefined) | |||
keys = incl; | |||
for (var k in keys) { | |||
if (inp[k] !== undefined && (excl === undefined || ! excl[k])) | |||
obj[k] = inp[k]; | |||
} | |||
return obj; | |||
}, | |||
// Create hash of 'true' entries for array/mongoose object | |||
arrayToHash: function (array) { | |||
var hash = {}; | |||
for (var e=0; e < array.length; e++) { | |||
hash[array[e]] = true; | |||
} | |||
return hash; | |||
}, | |||
// Log output session cookie | |||
debug: function (req) { | |||
console.log ("- " + req.headers.cookie + "\n+ " + req.session.id + "\n " + JSON.stringify (req.session)); | |||
}, | |||
// Init config data | |||
init: function () { | |||
this.config = JSON.parse (this.fs.readFileSync ("server_config.json")); | |||
}, | |||
}; | |||
module.exports = common; |
@@ -0,0 +1,31 @@ | |||
// Original file created by Prof.Dr. Matthias Hopf | |||
/* | |||
* Crypto routines for Authorization | |||
*/ | |||
const crypto = require ("crypto"); | |||
const defaultHash = "sha256"; | |||
const defaultSaltLen = 16; // More (e.g. 256) for extra paranoia | |||
const mod = { | |||
encodePwd: function (entry, pwd) { | |||
return crypto.createHash (entry.hash) .update (entry.salt + ":" + pwd, 'utf8') .digest ('base64'); | |||
}, | |||
checkLocalAuth: function (entry, pwd) { | |||
if (!entry || !entry._id || !entry.hash || !entry.salt || !entry.hash || !entry.pwd || | |||
!pwd || pwd === '') | |||
return false; | |||
return mod.encodePwd (entry, pwd) === entry.pwd; | |||
}, | |||
fillLocalAuth: function (entry, pwd) { | |||
if (!entry.hash) | |||
entry.hash = defaultHash; | |||
entry.salt = crypto.randomBytes (defaultSaltLen) .toString('base64'); | |||
entry.pwd = mod.encodePwd (entry, pwd); | |||
}, | |||
} | |||
module.exports = mod; |
@@ -0,0 +1,237 @@ | |||
/* | |||
* Main database access functions | |||
*/ | |||
var common, | |||
model = {}; | |||
const dbs = { | |||
/* Method API route | |||
* <- to server | |||
* -> to client | |||
* Description | |||
*/ | |||
routes: { | |||
"msg/ids": { | |||
/* GET /api/msg/ids [no args] | |||
* -> Array of message schema object ids | |||
* Get ALL known message ids | |||
*/ | |||
get: function(req, res) { | |||
model.Messages.find({},{_id: true}).exec() | |||
.then(results => { | |||
//selects id from message: | |||
var parsed = []; | |||
for (var i in results) { | |||
parsed.push (results[i]._id); | |||
} | |||
res.send(parsed); | |||
} ) | |||
.catch(err => { | |||
console.log (err); | |||
res.status(500).json(err); | |||
}); | |||
}, | |||
}, | |||
"msg/id": { | |||
params: ":id", | |||
/* GET /api/msg/id/[massage-id] | |||
* -> Message schema | |||
* Get a particular message | |||
*/ | |||
get: function(req, res) { | |||
model.Messages.findById(req.params.id) .exec(function(err, result) { | |||
if (err) { | |||
console.log (err); | |||
res.status(404).json(err); | |||
} else { | |||
//console.log(JSON.stringify(result)); | |||
res.json(result); | |||
} | |||
}); | |||
}, | |||
}, | |||
"msg/search": { | |||
params: ":phrase", | |||
/* GET /api/msg/id/[massage-id] | |||
* -> Message schema | |||
* Get a particular message | |||
*/ | |||
get: function(req, res) { | |||
model.Messages.find({$text: {$search: req.params.phrase}}) | |||
.exec().then(results => { | |||
//selects id from message: | |||
var parsed = []; | |||
for (var i in results) { | |||
parsed.push (results[i]._id); | |||
} | |||
res.send(parsed); | |||
} ) | |||
.catch(err => { | |||
console.log (err); | |||
res.status(500).json(err); | |||
}); | |||
}, | |||
}, | |||
"msg": { | |||
/* POST /api/msg | |||
* <- Message schema | |||
* -> Message schema | |||
* Create a new message | |||
*/ | |||
post: function(req, res) { | |||
/* | |||
if ( !(req.body.tags instanceof Array) ) { | |||
return res.status(400).json({ error: "bad request" }); | |||
}*/ | |||
//console.log("Subject: "+JSON.stringify(req.body)); | |||
model.Messages.create({ | |||
subject: req.body.subject, | |||
message: req.body.message, | |||
user: req.body.user, | |||
tag: req.body.tag | |||
}, function(err, result) { | |||
if (err) { | |||
return res.status(401).json(err.message); | |||
} else { | |||
res.json({message: "Message created!!"}); | |||
} | |||
if (result == null) { | |||
return res.status(500).json("Can not create message.") | |||
} | |||
}); | |||
}, | |||
/* PUT /api/msg | |||
* <- | |||
* -> | |||
* Update a message | |||
*/ | |||
//put: function(req, res) {}, | |||
}, | |||
"tag/ids": { | |||
/* GET /api/tag/ids [no args] | |||
* -> Array of tag schema object ids | |||
* Get ALL known tag ids | |||
*/ | |||
get: function(req, res) { | |||
model.Tags.find({},{_id: true}).exec() | |||
.then(results => { | |||
//selects id from tag: | |||
var parsed = []; | |||
for (var i in results) { | |||
parsed.push (results[i]._id); | |||
} | |||
res.send(parsed); | |||
} ) | |||
.catch(err => { | |||
console.log (err); | |||
res.status(500).json(err); | |||
}); | |||
}, | |||
}, | |||
"tag": { | |||
params: ":id", | |||
/* GET /api/tag/[tag-id] | |||
* -> Tag schema | |||
* Get a particular tag | |||
*/ | |||
get: function(req, res) { | |||
model.Tags.findById(req.params.id) .exec(function(err, result) { | |||
if (err) { | |||
console.log (err); | |||
res.status(404).json(err); | |||
} else { | |||
//console.log(JSON.stringify(result)); | |||
res.json(result); | |||
} | |||
}); | |||
}, | |||
}, | |||
}, | |||
/* Initialize requirements | |||
* - DB connection | |||
* - DB schemata | |||
*/ | |||
init: function (_common) { | |||
common = _common; | |||
/* DB Connection | |||
* Local db: common.config.dbLocalConn | |||
* TH db: common.config.dbConn | |||
*/ | |||
common.mongoose.connect (common.config.dbLocalConn, { | |||
useNewUrlParser: true | |||
}).then(() => { | |||
console.log("Database connected successfully."); | |||
}).catch(err => { | |||
console.log('Database connection error.'); | |||
process.exit(); | |||
}); | |||
/* DB Schemata | |||
* Privat fields: | |||
* - per model: _list: Elements that are included in list fetch | |||
* - per entry: _comment: Comment for Admin UI - TODO: not working yet | |||
*/ | |||
var messageSchema = common.mongoose.Schema({ | |||
subject: { type: String, required: true, | |||
_comment: "" }, | |||
message: { type: String, required: true, | |||
_comment: "" }, | |||
user: { type: String, required: true, | |||
_comment: "" }, | |||
tag: { type: [String], | |||
_comment: "" }, | |||
//createtime: { type: Date, default: Date.now }, | |||
}); | |||
messageSchema.index({ "$**":'text' }); | |||
model.Messages = common.mongoose.model('messages', messageSchema); | |||
model.Messages._list = [ "" ]; | |||
var tagSchema = common.mongoose.Schema({ | |||
name: { type: String, required: true, | |||
_comment: "" }, //unique | |||
}); | |||
model.Tags = common.mongoose.model('tags', messageSchema); | |||
model.Tags._list = [ "" ]; | |||
var userSchema = common.mongoose.Schema({ | |||
//_id: { type: String }, | |||
name: { type: String, required: true, | |||
_comment: "" }, | |||
pwd: { type: String, | |||
_comment: "" }, | |||
//hash: { type: String }, | |||
//salt: { type: String }, | |||
//type: { type: String }, | |||
roles: { type: [String], required: true, | |||
_comment: "" }, | |||
tags: { type: [String], | |||
_comment: "" }, | |||
//deactivated: { type: Boolean }, | |||
//participating: { type: [String] }, | |||
//host: { type: Boolean }, | |||
bookmarks: { type: [String], | |||
_comment: "" }, | |||
}); | |||
model.Users = common.mongoose.model('users', userSchema); | |||
model.Users._list = [ "" ]; | |||
}, | |||
models: model, | |||
}; | |||
/* | |||
app.get ("/api/msg/search/:phrase", function (req, res) { | |||
Message.find ({$text: {$search: req.params.phrase}) .then (function (err, results){ | |||
if (err) { | |||
console.log (err); | |||
res .status(404) .json (err); | |||
} else { | |||
console.log(JSON.stringify(results)); | |||
res.json(results); | |||
} | |||
}); | |||
}); | |||
*/ | |||
module.exports = dbs; |
@@ -0,0 +1,106 @@ | |||
// Original file created by Prof.Dr. Matthias Hopf | |||
/* | |||
* Valdiate ohm logins with ldap service | |||
*/ | |||
const ldap = require('ldapjs'); | |||
const ldap_escape = require('ldap-escape'); | |||
// TODO: Where do I get the URL from?? A: Is given. | |||
var ldap_client = ldap.createClient({ | |||
//url: 'ldap://gso2.ads1.fh-nuernberg.de/', | |||
url: 'ldap://sso.cs.ohm-hochschule.de:389/', | |||
//url: 'ldaps://sso.cs.ohm-hochschule.de:636/', | |||
reconnect: true, | |||
// timeouts don't work reliably | |||
}); | |||
// TODO: Where do I get the 'bindpath' parameters info from? A: Is given. | |||
const ldap_config = { | |||
bindpath: 'cn=Users,dc=ohm-hochschule,dc=de', | |||
timeout: 2000 | |||
}; | |||
const ldap_ohm = { | |||
init: function () { | |||
}, | |||
// Authorize user with password | |||
// Calls callback with null if unauthorized | |||
// Calls callback with object describing user if successful: | |||
authorize: function (user, pwd, cb) { | |||
if (typeof user != 'string' || typeof pwd != 'string') | |||
return cb (null); | |||
// Empty passwords *may* bind successfully anonymously | |||
if (user.length < 1 || pwd.length < 1) | |||
return cb (null); | |||
/* Same function, different writing style */ | |||
/* Escape ldap login input */ | |||
//escaped = ldap_escape.dn`cn=${user},`+ldap_config.bindpath; | |||
escaped = ldap_escape.dn (['cn=',','+ldap_config.bindpath], user); | |||
// Timeout handler: call callback, | |||
// make sure later ldap returns don't do anything weird | |||
var return_object = {}; | |||
var timeoutHandle = setTimeout (function () { | |||
console.log('ldap timeout'); | |||
return_object = null; | |||
cb (null); | |||
}, ldap_config.timeout); | |||
// Bind ldap to user (authorize) | |||
ldap_client.bind (escaped, pwd, function (err, res) { | |||
if (return_object === null) | |||
return; // Timeout, cb has already been called | |||
if (err !== null) { | |||
console.log ("ldap bind: failed for user " + user + ": " + err); | |||
clearTimeout (timeoutHandle); | |||
return cb (null); | |||
} | |||
// Search for user entry of just bound user | |||
// There should be only one... | |||
ldap_client.search (escaped, { sizeLimit: 1 }, function (err, res) { | |||
if (return_object === null) | |||
return; // Timeout, cb has already been called | |||
if (err !== null) { | |||
console.log ("ldap search: search after bind didn't work for user " | |||
+ user + ": " + err); | |||
clearTimeout (timeoutHandle); | |||
return cb (null); | |||
} | |||
// Populate return with search results | |||
res.on('searchEntry', function(entry) { | |||
if (return_object === null) | |||
return; // Timeout, cb has already been called | |||
return_object.user = user; | |||
return_object.name = entry.object.displayname; | |||
return_object.type = entry.object.employeetype; | |||
return_object.mail = entry.object.mail; | |||
return_object.gender = entry.object.orclgender; | |||
// Calling cb here, not in 'end', because of potential bugs with | |||
// concurrency failures, and we have our single(!) entry | |||
// https://github.com/joyent/node-ldapjs/pull/424 | |||
clearTimeout (timeoutHandle); | |||
if (typeof return_object.mail != 'string' || return_object.mail.length < 1) { | |||
console.log("ldap search error after bind for user " + user); | |||
return cb (null); | |||
} | |||
return cb (return_object); | |||
}); | |||
res.on('error', function(err) { | |||
console.log('ldap error: ' + err.message); | |||
}); | |||
res.on('end', function(result) { | |||
// TODO: Did we forget something? | |||
// TODO: analyze result.status? | |||
}); | |||
}); | |||
}); | |||
} | |||
}; | |||
module.exports = ldap_ohm; |
@@ -0,0 +1,35 @@ | |||
// Terminal call: node server/ldap_test.js - needs VPN or eduroam | |||
const inquirer = require('inquirer'), | |||
ldap = require('./ldap_ohm.js'); | |||
inquirer.prompt([ | |||
{ | |||
name: 'username', | |||
type: 'input', | |||
message: 'Enter your VirtuOhm username:', | |||
validate: function( value ) { | |||
if (value.length) { | |||
return true; | |||
} else { | |||
return 'Please enter your username.'; | |||
} | |||
} | |||
}, | |||
{ | |||
name: 'password', | |||
type: 'password', | |||
message: 'Enter your password:', | |||
validate: function(value) { | |||
if (value.length) { | |||
return true; | |||
} else { | |||
return 'Please enter your password.'; | |||
} | |||
} | |||
}]) | |||
.then(answers => { | |||
ldap.init(null); | |||
ldap.authorize(answers.username,answers.password,function(user) { | |||
console.log(JSON.stringify(user)); | |||
}); | |||
}); |
@@ -0,0 +1,9 @@ | |||
{ | |||
"dbConn": "mongodb://127.0.0.1:27017/om", | |||
"dbLocalConn": "mongodb://localhost:27017/omdb", | |||
"dbUser": "om", | |||
"dbPwd": "aeg1phuKeDaixese", | |||
"initialReservedTime": 120, | |||
"httpPort": 8013, | |||
"httpsPort": 8889 | |||
} |
@@ -1,8 +0,0 @@ | |||
const mongoose = require('mongoose'); | |||
const TagSchema = mongoose.Schema({ | |||
name: { type: String, required: true }, //unique | |||
}); | |||
module.exports = mongoose.model('Tag', TagSchema); |
@@ -1,14 +0,0 @@ | |||
const mongoose = require('mongoose'); | |||
const UserSchema = mongoose.Schema({ | |||
name: { type: String, required: true }, | |||
password: {type: String}, password: {type: String}, | |||
role: {type: String, required: true}, | |||
tags: [{type: String }], | |||
}); | |||
//tags as index: | |||
//UserSchema.index({tags:'text'}); | |||
module.exports = mongoose.model('User', UserSchema); |