rewrite for use linebridge 0.10.x

This commit is contained in:
srgooglo 2022-02-22 19:44:27 +01:00
parent fec72bcfb3
commit 6c29315a8e
24 changed files with 936 additions and 662 deletions

View File

@ -1,5 +1,5 @@
{
"name": "@ragestudio/server",
"name": "@comty/server",
"version": "0.15.0",
"main": "dist/index.js",
"scripts": {
@ -7,22 +7,22 @@
},
"license": "MIT",
"dependencies": {
"@corenode/utils": "^0.28.26",
"axios": "^0.24.0",
"bcrypt": "^5.0.1",
"connect-mongo": "^4.6.0",
"corenode": "^0.28.26",
"dicebar_lib": "^1.0.1",
"jsonwebtoken": "^8.5.1",
"linebridge": "^0.8.4",
"moment": "^2.29.1",
"mongoose": "^5.12.14",
"nanoid": "^3.1.23",
"passport": "^0.5.0",
"passport-jwt": "^4.0.0",
"passport-local": "^1.0.0",
"path-to-regexp": "^6.2.0",
"socket.io": "^4.2.0"
"@corenode/utils": "0.28.26",
"@nanoexpress/middleware-file-upload": "^1.0.6",
"axios": "0.25.0",
"bcrypt": "5.0.1",
"connect-mongo": "4.6.0",
"corenode": "0.28.26",
"dicebar_lib": "1.0.1",
"jsonwebtoken": "8.5.1",
"linebridge": "0.10.13",
"moment": "2.29.1",
"mongoose": "6.1.9",
"nanoid": "3.2.0",
"passport": "0.5.2",
"passport-jwt": "4.0.0",
"passport-local": "1.0.0",
"path-to-regexp": "6.2.0"
},
"devDependencies": {
"cross-env": "^7.0.3",

View File

@ -0,0 +1,12 @@
import { ComplexController } from "linebridge/dist/classes"
export default class ConfigController extends ComplexController {
static refName = "ConfigController"
static useMiddlewares = ["withAuthentication", "onlyAdmin"]
post = {
"/update_config": async (req, res) => {
},
}
}

View File

@ -0,0 +1,65 @@
import { ComplexController } from "linebridge/dist/classes"
import path from "path"
import fs from "fs"
import stream from "stream"
function resolveToUrl(filepath) {
return `${global.globalPublicURI}/uploads/${filepath}`
}
export default class FilesController extends ComplexController {
static refName = "FilesController"
get = {
"/uploads/:id": (req, res) => {
const filePath = path.join(global.uploadPath, req.params?.id)
const readStream = fs.createReadStream(filePath)
const passTrough = new stream.PassThrough()
stream.pipeline(readStream, passTrough, (err) => {
if (err) {
return res.status(400)
}
})
return passTrough.pipe(res)
}
}
post = {
"/upload": {
middlewares: ["withAuthentication", "fileUpload"],
fn: async (req, res) => {
const urls = []
const failed = []
if (!fs.existsSync(global.uploadPath)) {
await fs.promises.mkdir(global.uploadPath, { recursive: true })
}
if (req.files) {
for await (let file of req.files) {
try {
const filename = `${req.decodedToken.user_id}-${new Date().getTime()}-${file.filename}`
const diskPath = path.join(global.uploadPath, filename)
await fs.promises.writeFile(diskPath, file.data)
urls.push(resolveToUrl(filename))
} catch (error) {
console.log(error)
failed.push(file.filename)
}
}
}
return res.json({
urls: urls,
failed: failed,
})
}
}
}
}

View File

@ -0,0 +1,17 @@
import { ComplexController } from "linebridge/dist/classes"
export default class PublicController extends ComplexController {
static refName = "PublicController"
post = {
"/only_managers_test": {
middlewares: ["withAuthentication", "permissions"],
fn: (req, res) => {
return res.json({
message: "Congrats!, Only managers can access this route (or you are an admin)",
assertedPermissions: req.assertedPermissions
})
},
}
}
}

View File

@ -1,36 +1,113 @@
import { Role, User } from '../../models'
import { ComplexController } from "linebridge/dist/classes"
import { Role, User } from "../../models"
import { Schematized } from "../../lib"
export default {
get: Schematized({
select: ["user_id", "username"],
}, async (req, res) => {
const { user_id, username } = req.selection
export default class RolesController extends ComplexController {
static refName = "RolesController"
static useMiddlewares = ["roles"]
if (typeof user_id !== "undefined" || typeof username !== "undefined") {
const user = await User.findOne(req.selection)
if (!user) {
return res.status(404).json({ error: "No user founded" })
}
return res.json(user.roles)
}
get = {
"/roles": Schematized({
select: ["user_id", "username"],
}, async (req, res) => {
const roles = await Role.find()
const roles = await Role.find({})
return res.json(roles)
}),
"/user_roles": {
middlewares: ["withAuthentication"],
fn: Schematized({
select: ["username"],
}, async (req, res) => {
const user = await User.findOne(req.selection)
return res.json(roles)
}),
set: (req, res, next) => {
const { name, description } = req.body
Role.findOne({ name }).then((data) => {
if (data) {
return res.status(409).json("This role is already created")
}
let document = new Role({
name,
description
if (!user) {
return res.status(404).json({ error: "No user founded" })
}
return res.json(user.roles)
})
document.save()
return res.json(true)
})
},
}
post = {
"/role": {
middlewares: ["withAuthentication"],
fn: Schematized({
required: ["name"],
select: ["name", "description"],
}, async (req, res) => {
await Role.findOne(req.selection).then((data) => {
if (data) {
return res.status(409).json("This role is already created")
}
let role = new Role({
name: req.selection.name,
description: req.selection.description,
})
role.save()
return res.json(role)
})
})
},
"/update_user_roles": {
middlewares: ["withAuthentication"],
fn: Schematized({
required: ["update"],
select: ["update"],
}, async (req, res) => {
// check if issuer user is admin
if (!req.isAdmin()) {
return res.status(403).send("You do not have administrator permission")
}
if (!Array.isArray(req.selection.update)) {
return res.status(400).send("Invalid update request")
}
req.selection.update.forEach(async (update) => {
const user = await User.findById(update._id).catch(err => {
return false
})
console.log(update.roles)
if (user) {
user.roles = update.roles
await user.save()
}
})
return res.send("done")
}),
},
}
delete = {
"/role": {
middlewares: ["withAuthentication"],
fn: Schematized({
required: ["name"],
select: ["name"],
}, async (req, res) => {
if (req.selection.name === "admin") {
return res.status(409).json("You can't delete admin role")
}
await Role.findOne(req.selection).then((data) => {
if (!data) {
return res.status(404).json("This role is not found")
}
data.remove()
return res.json(data)
})
})
},
}
}

View File

@ -1,102 +1,110 @@
import { Session } from '../../models'
import jwt from 'jsonwebtoken'
import { Token } from '../../lib'
import { ComplexController } from "linebridge/dist/classes"
import { Session } from "../../models"
import jwt from "jsonwebtoken"
export default {
regenerate: async (req, res) => {
jwt.verify(req.jwtToken, req.jwtStrategy.secretOrKey, async (err, decoded) => {
if (err && !decoded?.allowRegenerate) {
return res.status(403).send("This token is invalid and is not allowed to be regenerated")
export default class SessionController extends ComplexController {
static refName = "SessionController"
get = {
"/sessions": {
middlewares: ["withAuthentication"],
fn: async (req, res) => {
// get current session _id
const { _id } = req.user
const sessions = await Session.find({ user_id: _id }, { token: 0 })
return res.json(sessions)
},
},
"/current_session": {
middlewares: ["withAuthentication"],
fn: async (req, res) => {
return res.json(req.currentSession)
}
},
}
const sessionToken = await Session.findOneAndDelete({ token: req.jwtToken, user_id: decoded.user_id })
if (sessionToken) {
delete decoded["iat"]
delete decoded["exp"]
delete decoded["date"]
post = {
"/validate_session": {
middlewares: ["useJwtStrategy"],
fn: async (req, res) => {
const token = req.body.session
const token = await Token.signNew({
...decoded,
}, req.jwtStrategy)
return res.json({ token })
}
})
},
deleteAll: async (req, res) => {
const { user_id } = req.body
if (typeof user_id === "undefined") {
return res.status(400).send("No user_id provided")
}
const allSessions = await Session.deleteMany({ user_id })
if (allSessions) {
return res.send("done")
}
return res.status(404).send("not found")
},
delete: async (req, res) => {
const { token, user_id } = req.body
if (typeof user_id === "undefined") {
return res.status(400).send("No user_id provided")
}
if (typeof token === "undefined") {
return res.status(400).send("No token provided")
}
const session = await Session.findOneAndDelete({ user_id, token })
if (session) {
return res.send("done")
}
return res.status(404).send("not found")
},
validate: async (req, res) => {
const token = req.body.session
let result = {
expired: false,
valid: true
}
await jwt.verify(token, req.jwtStrategy.secretOrKey, async (err, decoded) => {
if (err) {
result.valid = false
result.error = err.message
if (err.message === "jwt expired") {
result.expired = true
let result = {
expired: false,
valid: true
}
return
}
result = { ...result, ...decoded }
await jwt.verify(token, req.jwtStrategy.secretOrKey, async (err, decoded) => {
if (err) {
result.valid = false
result.error = err.message
const sessions = await Session.find({ user_id: result.user_id })
const sessionsTokens = sessions.map((session) => {
if (session.user_id === result.user_id) {
return session.token
if (err.message === "jwt expired") {
result.expired = true
}
return
}
result = { ...result, ...decoded }
const sessions = await Session.find({ user_id: result.user_id })
const sessionsTokens = sessions.map((session) => {
if (session.user_id === result.user_id) {
return session.token
}
})
if (!sessionsTokens.includes(token)) {
result.valid = false
result.error = "Session token not found"
} else {
result.valid = true
}
})
return res.json(result)
},
},
}
delete = {
"/session": {
middlewares: ["withAuthentication"],
fn: async (req, res) => {
const { token, user_id } = req.body
if (typeof user_id === "undefined") {
return res.status(400).send("No user_id provided")
}
if (typeof token === "undefined") {
return res.status(400).send("No token provided")
}
})
if (!sessionsTokens.includes(token)) {
result.valid = false
result.error = "Session token not found"
} else {
result.valid = true
const session = await Session.findOneAndDelete({ user_id, token })
if (session) {
return res.send("done")
}
return res.status(404).send("not found")
},
},
"/sessions": {
middlewares: ["withAuthentication"],
fn: async (req, res) => {
const { user_id } = req.body
if (typeof user_id === "undefined") {
return res.status(400).send("No user_id provided")
}
const allSessions = await Session.deleteMany({ user_id })
if (allSessions) {
return res.send("done")
}
return res.status(404).send("not found")
}
})
return res.json(result)
},
get: async (req, res) => {
// get current session _id
const { _id } = req.user
const sessions = await Session.find({ user_id: _id }, { token: 0 })
return res.json(sessions)
},
},
}
}

View File

@ -1,263 +1,195 @@
import passport from 'passport'
import bcrypt from 'bcrypt'
import { ComplexController } from "linebridge/dist/classes"
import passport from "passport"
import bcrypt from "bcrypt"
import { User } from '../../models'
import SessionController from '../SessionController'
import { Token, Schematized } from '../../lib'
import AvatarController from 'dicebar_lib'
import { User } from "../../models"
import SessionController from "../SessionController"
import { Token, Schematized } from "../../lib"
import AvatarController from "dicebar_lib"
import _ from "lodash"
import _ from 'lodash'
const AllowedUserUpdateKeys = [
"username",
"email",
"fullName",
]
export default {
isAuth: (req, res) => {
return res.json(`You look nice today 😎`)
},
getSelf: (req, res) => {
return res.json(req.user)
},
get: Schematized({
select: ["_id", "username"],
}, async (req, res) => {
let result = []
let selectQueryKeys = []
export default class UserController extends ComplexController {
static refName = "UserController"
if (Array.isArray(req.selection._id)) {
for await (let _id of req.selection._id) {
const user = await User.findById(_id).catch(err => {
get = {
"/self": {
middlewares: ["withAuthentication"],
fn: async (req, res) => {
return res.json(req.user)
},
},
"/user": {
middlewares: ["withAuthentication"],
fn: Schematized({
select: ["_id", "username"],
}, async (req, res) => {
const user = await User.findOne(req.selection)
if (!user) {
return res.status(404).json({ error: "User not exists" })
}
return res.json(user)
}),
},
"/users": {
middlewares: ["withAuthentication"],
fn: Schematized({
select: ["_id", "username"],
}, async (req, res) => {
let result = []
let selectQueryKeys = []
if (Array.isArray(req.selection._id)) {
for await (let _id of req.selection._id) {
const user = await User.findById(_id).catch(err => {
return false
})
if (user) {
result.push(user)
}
}
} else {
result = await User.find(req.selection, { username: 1, fullName: 1, _id: 1, roles: 1, avatar: 1 })
}
if (req.query?.select) {
try {
req.query.select = JSON.parse(req.query.select)
} catch (error) {
req.query.select = {}
}
selectQueryKeys = Object.keys(req.query.select)
}
if (selectQueryKeys.length > 0) {
result = result.filter(user => {
let pass = false
const selectFilter = req.query.select
selectQueryKeys.forEach(key => {
if (Array.isArray(selectFilter[key]) && Array.isArray(user[key])) {
// check if arrays includes any of the values
pass = selectFilter[key].some(val => user[key].includes(val))
} else if (typeof selectFilter[key] === "object" && typeof user[key] === "object") {
// check if objects includes any of the values
Object.keys(selectFilter[key]).forEach(objKey => {
pass = user[key][objKey] === selectFilter[key][objKey]
})
}
// check if strings includes any of the values
if (typeof selectFilter[key] === "string" && typeof user[key] === "string") {
pass = selectFilter[key].split(",").some(val => user[key].includes(val))
}
})
return pass
})
}
if (!result) {
return res.status(404).json({ error: "Users not found" })
}
return res.json(result)
})
},
}
post = {
"/login": async (req, res) => {
passport.authenticate("local", { session: false }, async (error, user, options) => {
if (error) {
return res.status(500).json(`Error validating user > ${error.message}`)
}
if (!user) {
return res.status(401).json("Invalid credentials")
}
const token = await Token.createNewAuthToken(user, options)
return res.json({ token: token })
})(req, res)
},
"/logout": {
middlewares: ["withAuthentication"],
fn: async (req, res, next) => {
req.body = {
user_id: req.decodedToken.user_id,
token: req.jwtToken
}
return SessionController.delete(req, res, next)
},
},
"/register": async (req, res) => {
User.findOne({ username: req.body.username })
.then((data) => {
if (data) {
return res.status(409).json("Username is already exists")
}
const avatar = AvatarController.generate({ seed: req.body.username, type: "initials" })
const hash = bcrypt.hashSync(req.body.password, parseInt(process.env.BCRYPT_ROUNDS))
let document = new User({
username: req.body.username,
fullName: req.body.fullName,
avatar: avatar.uri,
email: req.body.email,
password: hash
})
return document.save()
})
.then(data => {
return res.send(data)
})
.catch(err => {
return res.json(err)
})
},
"/update_user": {
middlewares: ["withAuthentication", "roles"],
fn: Schematized({
required: ["_id", "update"],
select: ["_id", "update"],
}, async (req, res) => {
let user = await User.findById(req.selection._id).catch(() => {
return false
})
if (user) {
result.push(user)
if (!user) {
return res.status(404).json({ error: "User not exists" })
}
}
} else {
result = await User.find(req.selection, { username: 1, fullName: 1, _id: 1, roles: 1, avatar: 1 })
}
if (req.query.select) {
try {
req.query.select = JSON.parse(req.query.select)
} catch (error) {
req.query.select = {}
}
if ((user._id.toString() !== req.user._id.toString()) && (req.hasRole("admin") === false)) {
return res.status(403).json({ error: "You are not allowed to update this user" })
}
selectQueryKeys = Object.keys(req.query.select)
}
if (selectQueryKeys.length > 0) {
result = result.filter(user => {
let pass = false
const selectFilter = req.query.select
selectQueryKeys.forEach(key => {
if (Array.isArray(selectFilter[key]) && Array.isArray(user[key])) {
// check if arrays includes any of the values
pass = selectFilter[key].some(val => user[key].includes(val))
} else if (typeof selectFilter[key] === 'object' && typeof user[key] === 'object') {
// check if objects includes any of the values
Object.keys(selectFilter[key]).forEach(objKey => {
pass = user[key][objKey] === selectFilter[key][objKey]
})
}
// check if strings includes any of the values
if (typeof selectFilter[key] === 'string' && typeof user[key] === 'string') {
pass = selectFilter[key].split(',').some(val => user[key].includes(val))
AllowedUserUpdateKeys.forEach((key) => {
if (typeof req.selection.update[key] !== "undefined") {
user[key] = req.selection.update[key]
}
})
return pass
})
}
if (!result) {
return res.status(404).json({ error: "Users not found" })
}
return res.json(result)
}),
getOne: Schematized({
select: ["_id", "username"],
}, async (req, res) => {
const user = await User.findOne(req.selection)
if (!user) {
return res.status(404).json({ error: "User not exists" })
}
return res.json(user)
}),
register: (req, res, next) => {
User.findOne({ username: req.body.username })
.then((data) => {
if (data) {
return res.status(409).json("Username is already exists")
}
const avatar = AvatarController.generate({ seed: req.body.username, type: "initials" })
const hash = bcrypt.hashSync(req.body.password, parseInt(process.env.BCRYPT_ROUNDS))
let document = new User({
username: req.body.username,
fullName: req.body.fullName,
avatar: avatar.uri,
email: req.body.email,
roles: ["registered"],
password: hash
})
return document.save()
})
.then(data => {
return res.send(data)
})
.catch(err => {
return next(err)
})
},
denyRole: async (req, res) => {
// check if issuer user is admin
if (!req.isAdmin()) {
return res.status(403).send("You do not have administrator permission")
}
let { user_id, username, roles } = req.body
const userQuery = {
username: username,
user_id: user_id,
}
// parse requested roles
if (typeof roles === "string") {
roles = roles.split(",").map(role => role.trim())
} else {
return res.send("No effect")
}
// get current user roles
const user = await User.findOne({ ...userQuery })
if (typeof user === "undefined") {
return res.status(404).send(`[${username}] User not found`)
}
// query all roles mutation
let queryRoles = []
if (Array.isArray(roles)) {
queryRoles = roles
} else if (typeof roles === 'string') {
queryRoles.push(roles)
}
// mutate all roles
if (queryRoles.length > 0 && Array.isArray(user.roles)) {
queryRoles.forEach(role => {
user.roles = user.roles.filter(_role => _role !== role)
})
}
// update user roles
await user.save()
return res.send("done")
},
grantRole: async (req, res) => {
// check if issuer user is admin
if (!req.isAdmin()) {
return res.status(403).send("You do not have administrator permission")
}
let { user_id, username, roles } = req.body
const userQuery = {
username: username,
user_id: user_id,
}
// parse requested roles
if (typeof roles === "string") {
roles = roles.split(",").map(role => role.trim())
} else {
return res.send("No effect")
}
// get current user roles
const user = await User.findOne({ ...userQuery })
if (typeof user === "undefined") {
return res.status(404).send(`[${username}] User not found`)
}
// query all roles mutation
let queryRoles = []
if (Array.isArray(roles)) {
queryRoles = roles
} else if (typeof roles === 'string') {
queryRoles.push(roles)
}
// mutate all roles
if (queryRoles.length > 0 && Array.isArray(user.roles)) {
queryRoles.forEach(role => {
if (!user.roles.includes(role)) {
user.roles.push(role)
}
})
}
// update user roles
await user.save()
return res.send("done")
},
updatePassword: async (req, res) => {
//TODO
},
updateSelf: async (req, res) => {
Object.keys(req.body).forEach(key => {
req.user[key] = req.body[key]
})
User.findOneAndUpdate({ _id: req.user._id }, req.user)
.then(() => {
return res.send(req.user)
})
.catch((err) => {
return res.send(500).send(err)
})
},
update: async (req, res) => {
// TODO
},
login: async (req, res) => {
passport.authenticate("local", { session: false }, async (error, user, options) => {
if (error) {
return res.status(500).json(`Error validating user > ${error.message}`)
}
if (!user) {
return res.status(401).json("Invalid credentials")
}
const payload = {
user_id: user._id,
username: user.username,
email: user.email
}
if (req.body.allowRegenerate) {
payload.allowRegenerate = true
}
// generate token
const token = Token.signNew(payload, options)
// send result
res.json({ token: token })
})(req, res)
},
logout: async (req, res, next) => {
req.body = {
user_id: req.decodedToken.user_id,
token: req.jwtToken
}
return SessionController.delete(req, res, next)
},
user.save()
.then(() => {
return res.send(user)
})
.catch((err) => {
return res.send(500).send(err)
})
}),
},
}
}

View File

@ -1,3 +1,15 @@
export { default as RolesController } from './RolesController'
export { default as SessionController } from './SessionController'
export { default as UserController } from './UserController'
import { default as ConfigController } from "./ConfigController"
import { default as RolesController } from "./RolesController"
import { default as SessionController } from "./SessionController"
import { default as UserController } from "./UserController"
import { default as FilesController } from "./FilesController"
import { default as PublicController } from "./PublicController"
export default [
ConfigController,
PublicController,
RolesController,
SessionController,
UserController,
FilesController,
]

View File

@ -1,109 +0,0 @@
module.exports = [
{
route: "/regenerate",
method: "POST",
middleware: ["ensureAuthenticated", "useJwtStrategy"],
fn: "SessionController.regenerate"
},
{
route: "/role",
method: 'PUT',
middleware: ["ensureAuthenticated", "roles"],
fn: "UserController.grantRole"
},
{
route: "/role",
method: 'DELETE',
middleware: ["ensureAuthenticated", "roles"],
fn: "UserController.denyRole"
},
{
route: "/roles",
method: "GET",
fn: "RolesController.get",
},
{
route: "/session",
method: 'DELETE',
middleware: "ensureAuthenticated",
fn: "SessionController.delete",
},
{
route: "/sessions",
method: 'DELETE',
middleware: "ensureAuthenticated",
fn: "SessionController.deleteAll",
},
{
route: "/validate_session",
method: "POST",
middleware: "useJwtStrategy",
fn: "SessionController.validate",
},
{
route: "/sessions",
method: "GET",
middleware: "ensureAuthenticated",
fn: "SessionController.get",
},
{
route: "/has_permissions",
method: "POST",
middleware: [
"ensureAuthenticated",
"hasPermissions"
]
},
{
route: "/self",
method: "GET",
middleware: "ensureAuthenticated",
fn: "UserController.getSelf",
},
{
route: "/users",
method: "GET",
middleware: "ensureAuthenticated",
fn: "UserController.get",
},
{
route: "/user",
method: "GET",
middleware: "ensureAuthenticated",
fn: "UserController.getOne",
},
{
route: "/self_user",
method: "PUT",
middleware: "ensureAuthenticated",
fn: "UserController.updateSelf",
},
{
route: "/user",
method: "PUT",
middleware: ["ensureAuthenticated", "privileged"],
fn: "UserController.update",
},
{
route: "/login",
method: "POST",
fn: "UserController.login",
},
{
route: "/logout",
method: "POST",
middleware: ["ensureAuthenticated"],
fn: "UserController.logout",
},
{
route: "/register",
method: "POST",
fn: "UserController.register",
},
{
route: "/is_auth",
method: "POST",
middleware: "ensureAuthenticated",
fn: "UserController.isAuth",
}
]

View File

@ -1,94 +1,107 @@
import LinebridgeServer from 'linebridge/server'
import bcrypt from 'bcrypt'
import mongoose from 'mongoose'
import passport from 'passport'
import { User } from './models'
import socketIo from 'socket.io'
Array.prototype.updateFromObjectKeys = function (obj) {
this.forEach((value, index) => {
if (obj[value] !== undefined) {
this[index] = obj[value]
}
})
const b64Decode = global.b64Decode = (data) => {
return Buffer.from(data, 'base64').toString('utf-8')
return this
}
const b64Encode = global.b64Encode = (data) => {
return Buffer.from(data, 'utf-8').toString('base64')
}
import path from "path"
import LinebridgeServer from "linebridge/dist/server"
import bcrypt from "bcrypt"
import mongoose from "mongoose"
import passport from "passport"
import { User, Session } from "./models"
import socketIo from "socket.io"
import jwt from "jsonwebtoken"
const JwtStrategy = require('passport-jwt').Strategy
const ExtractJwt = require('passport-jwt').ExtractJwt
const LocalStrategy = require('passport-local').Strategy
const { Buffer } = require("buffer")
const b64Decode = global.b64Decode = (data) => {
return Buffer.from(data, "base64").toString("utf-8")
}
const b64Encode = global.b64Encode = (data) => {
return Buffer.from(data, "utf-8").toString("base64")
}
const ExtractJwt = require("passport-jwt").ExtractJwt
const LocalStrategy = require("passport-local").Strategy
function parseConnectionString(obj) {
const { db_user, db_driver, db_name, db_pwd, db_hostname, db_port } = obj
return `${db_driver}://${db_user}:${db_pwd}@${db_hostname}:${db_port}/${db_name}`
}
class Server {
constructor() {
this.env = _env
this.env = process.env
this.listenPort = this.env.listenPort ?? 3000
this.wsListenPort = this.env.wsListenPort ?? 3001
this.controllers = require("./controllers").default
this.middlewares = require("./middlewares")
this.controllers = require("./controllers")
this.endpoints = require("./endpoints")
this.instance = new LinebridgeServer({
listen: "0.0.0.0",
middlewares: this.middlewares,
controllers: this.controllers,
endpoints: this.endpoints,
port: this.listenPort
})
port: this.listenPort,
wsPort: this.wsListenPort,
headers: {
"Access-Control-Expose-Headers": "regenerated_token",
},
onWSClientConnection: this.onWSClientConnection,
onWSClientDisconnection: this.onWSClientDisconnection,
}, this.controllers, this.middlewares)
this.server = this.instance.httpServer
this.io = new socketIo.Server(3001, {
maxHttpBufferSize: 100000000,
connectTimeout: 5000,
transports: ['websocket', 'polling'],
pingInterval: 25 * 1000,
pingTimeout: 5000,
allowEIO3: true,
cors: {
origin: "http://localhost:8000",
methods: ["GET", "POST"],
}
}).of("/main")
this.server = this.instance.httpInterface
this.options = {
jwtStrategy: {
sessionLocationSign: this.instance.id,
jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
secretOrKey: this.instance.oskid,
algorithms: ['sha1', 'RS256', 'HS256'],
expiresIn: "1h"
algorithms: ["sha1", "RS256", "HS256"],
expiresIn: this.env.signLifetime ?? "1h",
}
}
this.instance.wsInterface["clients"] = []
this.instance.wsInterface["findUserIdFromClientID"] = (searchClientId) => {
return this.instance.wsInterface.clients.find(client => client.id === searchClientId)?.userId ?? false
}
this.instance.wsInterface["getClientSockets"] = (userId) => {
return this.instance.wsInterface.clients.filter(client => client.userId === userId).map((client) => {
return client?.socket
})
}
this.instance.wsInterface["broadcast"] = async (channel, ...args) => {
for await (const client of this.instance.wsInterface.clients) {
client.socket.emit(channel, ...args)
}
}
global.wsInterface = this.instance.wsInterface
global.httpListenPort = this.listenPort
global.globalPublicURI = this.env.globalPublicURI
global.uploadPath = this.env.uploadPath ?? path.resolve(process.cwd(), "uploads")
global.jwtStrategy = this.options.jwtStrategy
global.signLocation = this.env.signLocation
this.initialize()
}
async initialize() {
await this.connectToDB()
await this.initPassport()
await this.initWebsockets()
// register middlewares
this.instance.middlewares["useJwtStrategy"] = (req, res, next) => {
req.jwtStrategy = this.options.jwtStrategy
next()
}
this.io.on("connection", (socket) => {
console.log(socket.id)
})
await this.instance.init()
}
getDBConnectionString() {
const { db_user, db_driver, db_name, db_pwd, db_hostname, db_port } = _env
return `${db_driver}://${db_user}:${db_pwd}@${db_hostname}:${db_port}/${db_name}`
await this.instance.initialize()
}
connectToDB = () => {
return new Promise((resolve, reject) => {
try {
console.log("🌐 Trying to connect to DB...")
mongoose.connect(this.getDBConnectionString(), { useNewUrlParser: true, useFindAndModify: false })
mongoose.connect(parseConnectionString(this.env), { useNewUrlParser: true, useUnifiedTopology: true })
.then((res) => { return resolve(true) })
.catch((err) => { return reject(err) })
} catch (err) {
@ -105,26 +118,22 @@ class Server {
})
}
setWebsocketRooms = () => {
this.ws.register("/test", {
onOpen: (socket) => {
console.log(socket)
setInterval(() => {
socket.send("Hello")
}, 1000)
}
})
this.ws.listen()
}
initPassport() {
this.instance.middlewares["useJwtStrategy"] = (req, res, next) => {
req.jwtStrategy = this.options.jwtStrategy
next()
}
this.instance.middlewares["useWS"] = (req, res, next) => {
req.ws = global.wsInterface
next()
}
passport.use(new LocalStrategy({
usernameField: "username",
passwordField: "password",
session: false
}, (username, password, done) => {
User.findOne({ username: b64Decode(username) }).select('+password')
User.findOne({ username: b64Decode(username) }).select("+password")
.then((data) => {
if (data === null) {
return done(null, false, this.options.jwtStrategy)
@ -138,22 +147,97 @@ class Server {
.catch(err => done(err, null, this.options.jwtStrategy))
}))
passport.use(new JwtStrategy(this.options.jwtStrategy, (token, callback) => {
User.findOne({ _id: token.user_id })
.then((data) => {
if (data === null) {
return callback(null, false)
} else {
return callback(null, data, token)
}
})
.catch((err) => {
return callback(err, null)
})
}))
this.server.use(passport.initialize())
}
initWebsockets() {
const onAuthenticated = (socket, user_id) => {
this.attachClientSocket(socket, user_id)
socket.emit("authenticated")
}
const onAuthenticatedFailed = (socket, error) => {
this.detachClientSocket(socket)
socket.emit("authenticateFailed", {
error,
})
}
this.instance.wsInterface.eventsChannels.push(["/main", "authenticate", async (socket, token) => {
const session = await Session.findOne({ token }).catch(err => {
return false
})
if (!session) {
return onAuthenticatedFailed(socket, "Session not found")
}
this.verifyJwt(token, async (err, decoded) => {
if (err) {
return onAuthenticatedFailed(socket, err)
} else {
const user = await User.findById(decoded.user_id).catch(err => {
return false
})
if (!user) {
return onAuthenticatedFailed(socket, "User not found")
}
return onAuthenticated(socket, user)
}
})
}])
}
onWSClientConnection = async (socket) => {
console.log(`🌐 Client connected: ${socket.id}`)
}
onWSClientDisconnection = async (socket) => {
console.log(`🌐 Client disconnected: ${socket.id}`)
this.detachClientSocket(socket)
}
attachClientSocket = async (client, userData) => {
const socket = this.instance.wsInterface.clients.find(c => c.id === client.id)
if (socket) {
socket.socket.disconnect()
}
const clientObj = {
id: client.id,
socket: client,
userId: userData._id.toString(),
user: userData,
}
this.instance.wsInterface.clients.push(clientObj)
this.instance.wsInterface.io.emit("userConnected", userData)
}
detachClientSocket = async (client) => {
const socket = this.instance.wsInterface.clients.find(c => c.id === client.id)
if (socket) {
socket.socket.disconnect()
this.instance.wsInterface.clients = this.instance.wsInterface.clients.filter(c => c.id !== client.id)
}
this.instance.wsInterface.io.emit("userDisconnect", client.id)
}
verifyJwt = (token, callback) => {
jwt.verify(token, this.options.jwtStrategy.secretOrKey, async (err, decoded) => {
if (err) {
return callback(err)
}
return callback(null, decoded)
})
}
}
new Server()

View File

@ -0,0 +1,70 @@
// random 5 digits number
const random5 = () => Math.floor(Math.random() * 90000) + 10000
// secure random 5 digits number
const random5Secure = () => {
const random = random5()
return random.toString().padStart(5, '0')
}
// aa-bbbbb-cccc
//* a: type (2 digits)
//* b: serial (5 digits)
//* c: manufacturer (4 digits)
const typesNumber = {
"computers-desktop": [1],
"computers-laptop": [2],
"computers-tablet": [3],
"computers-smartphone": [4],
"networking": [5],
"peripherals-printer": [6],
"peripherals-monitor": [7],
}
export function genV1(params) {
let { type, serial, manufacturer } = params // please in that order
type = type.toLowerCase()
let str = []
// Type parsing
let typeBuf = []
if (typeof typesNumber[type] === "undefined") {
typeBuf[0] = 0
typeBuf[1] = "X"
} else {
typeBuf[0] = typesNumber[type][0]
typeBuf[1] = typesNumber[type][1] ?? "X"
}
str.push(typeBuf.join(""))
// Serial parsing
// if serial is not defined, generate a random 4 digits number
if (typeof serial === "undefined") {
str.push(random5().toString())
} else {
// push last 5 digits of serial, if serial is not 5 digits, pad with 0
let serialBuf = []
serialBuf[0] = serial.slice(-5, -4) ?? "0"
serialBuf[1] = serial.slice(-4, -3) ?? "0"
serialBuf[2] = serial.slice(-3, -2) ?? "0"
serialBuf[3] = serial.slice(-2, -1) ?? "0"
serialBuf[4] = serial.slice(-1) ?? "0"
str.push(serialBuf.join(""))
}
// Manufacturer parsing
// abreviate manufacturer name to 4 letters
if (typeof manufacturer === "undefined") {
str.push("GENR")
} else {
str.push(manufacturer.slice(0, 4).toUpperCase())
}
return str.join("-")
}

View File

@ -1,9 +1,12 @@
export default (schema = {}, fn) => {
return async (req, res, next) => {
// if is nullish
req.body = req.body ?? {}
req.query = req.query ?? {}
if (typeof req.body === "undefined") {
req.body = {}
}
if (typeof req.query === "undefined") {
req.query = {}
}
if (schema.required) {
if (Array.isArray(schema.required)) {
const missingKeys = []

View File

@ -1,29 +1,54 @@
import jwt from 'jsonwebtoken'
import { nanoid } from 'nanoid'
import { Session } from '../../models'
import jwt from "jsonwebtoken"
import { nanoid } from "nanoid"
import { Session, User } from "../../models"
export function signNew(payload, options) {
const data = {
uuid: nanoid(),
allowRegenerate: false,
...payload
export async function createNewAuthToken(user, options = {}) {
const payload = {
user_id: user._id,
username: user.username,
email: user.email,
refreshToken: nanoid(),
signLocation: global.signLocation,
}
const token = jwt.sign(data, options.secretOrKey, {
await User.findByIdAndUpdate(user._id, { refreshToken: payload.refreshToken })
return await signNew(payload, options)
}
export async function signNew(payload, options = {}) {
if (options.updateSession) {
const sessionData = await Session.findById(options.updateSession)
payload.session_uuid = sessionData.session_uuid
} else {
payload.session_uuid = nanoid()
}
const token = jwt.sign(payload, options.secretOrKey, {
expiresIn: options.expiresIn ?? "1h",
algorithm: options.algorithm ?? "HS256"
})
let newSession = new Session({
uuid: data.uuid,
user_id: data.user_id,
allowRegenerate: data.allowRegenerate,
const session = {
token: token,
session_uuid: payload.session_uuid,
username: payload.username,
user_id: payload.user_id,
date: new Date().getTime(),
location: options.sessionLocationSign
})
location: payload.signLocation ?? "rs-auth",
}
newSession.save()
if (options.updateSession) {
await Session.findByIdAndUpdate(options.updateSession, {
token: session.token,
date: session.date,
location: session.location,
})
} else {
let newSession = new Session(session)
newSession.save()
}
return token
}

View File

@ -1,40 +0,0 @@
import passport from 'passport'
import { Session } from '../../models'
export default (req, res, next) => {
function unauthorized() {
console.log("Returning failed session")
return res.status(401).send({ error: 'Invalid session', })
}
const authHeader = req.headers?.authorization?.split(' ')
if (authHeader && authHeader[0] === 'Bearer') {
const token = authHeader[1]
passport.authenticate('jwt', { session: false }, async (err, user, decodedToken) => {
if (err) {
return res.status(500).send({ error: err.message })
}
if (!user) {
return res.status(404).send({ error: "No user data found" })
}
const sessions = await Session.find({ user_id: decodedToken.user_id })
const sessionsTokens = sessions.map(session => session.token)
if (!sessionsTokens.includes(token)) {
return unauthorized()
}
req.user = user
req.jwtToken = token
req.decodedToken = decodedToken
return next()
})(req, res, next)
} else {
return unauthorized()
}
}

View File

@ -1,4 +1,10 @@
export { default as ensureAuthenticated } from './ensureAuthenticated'
export { default as errorHandler } from './errorHandler'
export { default as hasPermissions } from './hasPermissions'
export { default as roles } from './roles'
const fileUpload = require("@nanoexpress/middleware-file-upload/cjs")()
export { default as withAuthentication } from "./withAuthentication"
export { default as errorHandler } from "./errorHandler"
export { default as hasPermissions } from "./hasPermissions"
export { default as roles } from "./roles"
export { default as onlyAdmin } from "./onlyAdmin"
export { default as permissions } from "./permissions"
export { fileUpload as fileUpload }

View File

@ -1,6 +1,6 @@
export default (req, res, next) => {
if (!req.user.roles.includes("admin")) {
return res.status(401).send({ error: "To make this request it is necessary to have administrator permissions" })
return res.status(403).send({ error: "To make this request it is necessary to have administrator permissions" })
}
next()

View File

@ -0,0 +1,39 @@
import { Config } from "../../models"
export default (req, res, next) => {
const requestedPath = `${req.method.toLowerCase()}${req.path.toLowerCase()}`
Config.findOne({ key: "permissions" }, undefined, {
lean: true,
}).then(({ value }) => {
req.assertedPermissions = []
const pathRoles = value.pathRoles ?? {}
if (typeof pathRoles[requestedPath] === "undefined") {
console.warn(`[Permissions] No permissions defined for path ${requestedPath}`)
return next()
}
const requiredRoles = Array.isArray(pathRoles[requestedPath]) ? pathRoles[requestedPath] : [pathRoles[requestedPath]]
requiredRoles.forEach((role) => {
if (req.user.roles.includes(role)) {
req.assertedPermissions.push(role)
}
})
if (req.user.roles.includes("admin")) {
req.assertedPermissions.push("admin")
}
if (req.assertedPermissions.length === 0 && !req.user.roles.includes("admin")) {
return res.status(403).json({
error: "forbidden",
message: "You don't have permission to access this resource",
})
}
next()
})
}

View File

@ -7,5 +7,13 @@ export default (req, res, next) => {
return false
}
req.hasRole = (role) => {
if (req.user.roles.includes(role)) {
return true
}
return false
}
next()
}

View File

@ -0,0 +1,63 @@
import { Session, User } from "../../models"
import { Token } from "../../lib"
import jwt from "jsonwebtoken"
export default (req, res, next) => {
function reject(description) {
return res.status(401).send({ error: `${description ?? "Invalid session"}` })
}
const authHeader = req.headers?.authorization?.split(" ")
if (authHeader && authHeader[0] === "Bearer") {
const token = authHeader[1]
let decoded = null
try {
decoded = jwt.decode(token)
} catch (error) {
console.error(error)
}
if (!decoded) {
return reject("Cannot decode token")
}
jwt.verify(token, global.jwtStrategy.secretOrKey, async (err) => {
const sessions = await Session.find({ user_id: decoded.user_id })
const currentSession = sessions.find((session) => session.token === token)
if (!currentSession) {
return reject("Cannot find session")
}
const userData = await User.findOne({ _id: currentSession.user_id }).select("+refreshToken")
if (!userData) {
return res.status(404).send({ error: "No user data found" })
}
if (err) {
if (decoded.refreshToken === userData.refreshToken) {
const regeneratedToken = await Token.createNewAuthToken(userData, {
...global.jwtStrategy,
updateSession: currentSession._id,
})
res.setHeader("regenerated_token", regeneratedToken)
} else {
return reject("Token expired, cannot refresh token either")
}
}
req.user = userData
req.jwtToken = token
req.decodedToken = decoded
req.currentSession = currentSession
return next()
})
} else {
return reject("Missing token header")
}
}

View File

@ -1,11 +1,10 @@
import mongoose from 'mongoose'
import { Schema } from 'mongoose'
import mongoose, { Schema } from "mongoose"
function getSchemas() {
const obj = Object()
const _schemas = require("../schemas")
Object.keys(_schemas).forEach(key => {
Object.keys(_schemas).forEach((key) => {
obj[key] = Schema(_schemas[key])
})
@ -14,6 +13,7 @@ function getSchemas() {
const schemas = getSchemas()
export const Role = mongoose.model('Role', schemas.Role, 'roles')
export const User = mongoose.model('User', schemas.User, "accounts")
export const Session = mongoose.model('Session', schemas.Session, "sessions")
export const Config = mongoose.model("Config", schemas.Config, "config")
export const User = mongoose.model("User", schemas.User, "accounts")
export const Session = mongoose.model("Session", schemas.Session, "sessions")
export const Role = mongoose.model("Role", schemas.Role, "roles")

View File

@ -0,0 +1,3 @@
export default {
key: { type: String, required: true },
}

View File

@ -1,3 +1,4 @@
export { default as User } from './user'
export { default as Role } from './role'
export { default as Session } from './session'
export { default as User } from "./user"
export { default as Role } from "./role"
export { default as Session } from "./session"
export { default as Config } from "./config"

View File

@ -1,9 +1,8 @@
export default {
allowRegenerate: { type: Boolean, default: false },
uuid: { type: String, required: true },
session_uuid: { type: String, required: true },
token: { type: String, required: true },
username: { type: String, required: true },
user_id: { type: String, required: true },
date: { type: Number, default: 0 },
location: { type: String, default: "Unknown" },
geo: { type: String, default: "Unknown" },
}

View File

@ -1,10 +1,9 @@
export default {
refreshToken: { type: String, select: false },
username: { type: String, required: true },
password: { type: String, required: true, select: false },
fullName: String,
avatar: { type: String },
email: String,
roles: [],
legal_id: Object,
phone: Number,
roles: { type: Array, default: [] },
}