commit from local

This commit is contained in:
SrGooglo 2024-03-05 13:11:23 +00:00
parent b415f024b5
commit c7f8d33aa6
29 changed files with 732 additions and 30 deletions

View File

@ -7,7 +7,7 @@
3004 -> chats
3005 -> marketplace
3006 -> sync
3007 -> mail
3007 -> ems (External Messaging Service)
3008 -> unallocated
3009 -> unallocated
3010 -> unallocated
@ -16,3 +16,8 @@
3013 -> unallocated
3014 -> unallocated
3015 -> unallocated
3016 -> unallocated
3017 -> unallocated
3018 -> unallocated
3019 -> unallocated
3020 -> auth

View File

@ -1,4 +1,5 @@
#!/usr/bin/env node
require("dotenv").config()
require("sucrase/register")
const path = require("path")
@ -28,6 +29,7 @@ global["aliases"] = {
"@shared-utils": path.resolve(__dirname, "utils"),
"@shared-classes": path.resolve(__dirname, "classes"),
"@shared-lib": path.resolve(__dirname, "lib"),
"@shared-middlewares": path.resolve(__dirname, "middlewares"),
// expose internal resources
"@lib": path.resolve(global["__src"], "lib"),

View File

@ -0,0 +1,237 @@
import jwt from "jsonwebtoken"
import { Session, RegenerationToken, User } from "../../classes/DbModels"
export default class Token {
static get strategy() {
return {
secret: process.env.JWT_SECRET,
expiresIn: process.env.JWT_EXPIRES_IN ?? "1h",
algorithm: process.env.JWT_ALGORITHM ?? "HS256",
}
}
static async createNewAuthToken(payload, options = {}) {
if (options.updateSession) {
const sessionData = await Session.findOne({ _id: options.updateSession })
payload.session_uuid = sessionData.session_uuid
} else {
payload.session_uuid = global.nanoid()
}
const { secret, expiresIn, algorithm } = Token.strategy
const token = jwt.sign(
{
session_uuid: payload.session_uuid,
username: payload.username,
user_id: payload.user_id,
signLocation: payload.signLocation,
},
secret,
{
expiresIn: expiresIn,
algorithm: algorithm
}
)
return token
}
static async validate(token) {
let result = {
expired: false,
valid: true,
error: null,
data: null,
}
if (typeof token === "undefined") {
result.valid = false
result.error = "Missing token"
return result
}
const { secret } = Token.strategy
await jwt.verify(token, secret, async (err, decoded) => {
if (err) {
result.valid = false
result.error = err.message
if (err.message === "jwt expired") {
result.expired = true
}
return
}
result.data = decoded
const sessions = await Session.find({ user_id: decoded.user_id })
const currentSession = sessions.find((session) => session.token === token)
if (!currentSession) {
result.valid = false
result.error = "Session token not found"
} else {
result.session = currentSession
result.valid = true
result.user = async () => await User.findOne({ _id: decoded.user_id })
}
})
return result
}
static async regenerate(expiredToken, refreshToken, aggregateData = {}) {
// search for a regeneration token with the expired token (Should exist only one)
const regenerationToken = await RegenerationToken.findOne({ refreshToken: refreshToken })
if (!regenerationToken) {
throw new Error("Cannot find regeneration token")
}
// check if the regeneration token is valid and not expired
let decodedRefreshToken = null
let decodedExpiredToken = null
try {
decodedRefreshToken = jwt.decode(refreshToken)
decodedExpiredToken = jwt.decode(expiredToken)
} catch (error) {
console.error(error)
// TODO: Storage this incident
}
if (!decodedRefreshToken) {
throw new Error("Cannot decode refresh token")
}
if (!decodedExpiredToken) {
throw new Error("Cannot decode expired token")
}
// is not needed to verify the expired token, because it suppossed to be expired
// verify refresh token
await jwt.verify(refreshToken, global.jwtStrategy.secretOrKey, async (err) => {
// check if is expired
if (err) {
if (err.message === "jwt expired") {
// check if server has enabled the enforcement of regeneration token expiration
if (global.jwtStrategy.enforceRegenerationTokenExpiration) {
// delete the regeneration token
await RegenerationToken.deleteOne({ refreshToken: refreshToken })
throw new Error("Regeneration token expired and cannot be regenerated due server has enabled enforcement security policy")
}
}
}
// check if the regeneration token is associated with the expired token
if (decodedRefreshToken.expiredToken !== expiredToken) {
throw new Error("Regeneration token is not associated with the expired token")
}
})
// find the session associated with the expired token
const session = await Session.findOne({ token: expiredToken })
if (!session) {
throw new Error("Cannot find session associated with the expired token")
}
// generate a new token
const newToken = await this.createNewAuthToken({
username: decodedExpiredToken.username,
session_uuid: session.session_uuid,
user_id: decodedExpiredToken.user_id,
ip_address: aggregateData.ip_address,
}, {
updateSession: session._id,
})
// delete the regeneration token
await RegenerationToken.deleteOne({ refreshToken: refreshToken })
return newToken
}
static async createAuth(payload, options = {}) {
const token = await this.createNewAuthToken(payload, options)
const session = {
token: token,
session_uuid: payload.session_uuid,
username: payload.username,
user_id: payload.user_id,
location: payload.signLocation,
ip_address: payload.ip_address,
client: payload.client,
date: new Date().getTime(),
}
if (options.updateSession) {
await Session.findByIdAndUpdate(options.updateSession, session)
} else {
let newSession = new Session(session)
await newSession.save()
}
return token
}
static async createRegenerative(expiredToken) {
// check if token is only expired, if is corrupted, reject
let decoded = null
try {
decoded = jwt.decode(expiredToken)
} catch (error) {
console.error(error)
}
if (!decoded) {
return false
}
// check if token exists on a session
const sessions = await Session.find({ user_id: decoded.user_id })
const currentSession = sessions.find((session) => session.token === expiredToken)
if (!currentSession) {
throw new Error("This token is not associated with any session")
}
// create a new refresh token and sign it with maximum expiration time of 1 day
const refreshToken = jwt.sign(
{
expiredToken
},
global.jwtStrategy.secretOrKey,
{
expiresIn: "1d"
}
)
// create a new regeneration token and save it
const regenerationToken = new RegenerationToken({
expiredToken,
refreshToken,
})
await regenerationToken.save()
// return the regeneration token
return regenerationToken
}
static async getRegenerationToken(expiredToken) {
const regenerationToken = await RegenerationToken.findOne({ expiredToken })
return regenerationToken
}
}

View File

@ -18,6 +18,7 @@ function getConnectionConfig(obj) {
dbName: DB_NAME,
user: DB_USER,
pass: DB_PWD,
maxPoolSize: 100,
}
if (DB_AUTH_SOURCE) {

View File

@ -50,27 +50,30 @@ export default () => {
clientOptions = composeURL(clientOptions)
let client = {}
let client = new Redis(clientOptions.host, clientOptions.port, clientOptions)
client.initialize = async () => {
console.log(`🔌 Connecting to Redis client [${REDIS_HOST}]`)
client.on("error", (error) => {
console.error("❌ Redis client error:", error)
})
client = new Redis(clientOptions)
client.on("connect", () => {
console.log(`✅ Redis client connected [${process.env.REDIS_HOST}]`)
})
client.on("error", (error) => {
console.error("❌ Redis client error:", error)
client.on("reconnecting", () => {
console.log("🔄 Redis client reconnecting...")
})
const initialize = async () => {
return await new Promise((resolve, reject) => {
console.log(`🔌 Connecting to Redis client [${REDIS_HOST}]`)
client.connect(resolve)
})
client.on("connect", () => {
console.log(`✅ Redis client connected [${process.env.REDIS_HOST}]`)
})
client.on("reconnecting", () => {
console.log("🔄 Redis client reconnecting...")
})
return client
}
return client
return {
client,
initialize
}
}

View File

@ -0,0 +1,133 @@
import crypto from "crypto"
export default class SecureEntry {
constructor(model, params = {}) {
this.params = params
if (!model) {
throw new Error("Missing model")
}
this.model = model
}
static get encrytionAlgorithm() {
return "aes-256-cbc"
}
async set(key, value, {
keyName = "key",
valueName = "value",
}) {
if (!keyName) {
throw new Error("Missing keyName")
}
if (!valueName) {
throw new Error("Missing valueName")
}
if (!key) {
throw new Error("Missing key")
}
if (!value) {
throw new Error("Missing value")
}
let entry = await this.model.findOne({
[keyName]: key,
[valueName]: value,
}).catch(() => null)
const encryptionKey = Buffer.from(process.env.SYNC_ENCRIPT_SECRET, "hex")
const iv = crypto.randomBytes(16)
const cipher = crypto.createCipheriv(SecureEntry.encrytionAlgorithm, encryptionKey, iv)
let encryptedData
try {
encryptedData = cipher.update(value)
}
catch (error) {
console.error(error)
}
encryptedData = Buffer.concat([encryptedData, cipher.final()])
value = iv.toString("hex") + ":" + encryptedData.toString("hex")
if (entry) {
entry[valueName] = value
await entry.save()
return entry
}
entry = new this.model({
[keyName]: key,
[valueName]: value,
})
await entry.save()
return entry
}
async get(key, value, {
keyName = "key",
valueName = "value",
}) {
if (!keyName) {
throw new Error("Missing keyName")
}
if (!key) {
throw new Error("Missing key")
}
const searchQuery = {
[keyName]: key,
}
if (value) {
searchQuery[valueName] = value
}
const entry = await this.model.findOne(searchQuery).catch(() => null)
if (!entry || !entry[valueName]) {
return null
}
const encryptionKey = Buffer.from(process.env.SYNC_ENCRIPT_SECRET, "hex")
const iv = Buffer.from(entry[valueName].split(":")[0], "hex")
const encryptedText = Buffer.from(entry[valueName].split(":")[1], "hex")
const decipher = crypto.createDecipheriv(SecureEntry.encrytionAlgorithm, encryptionKey, iv)
let decrypted = decipher.update(encryptedText)
decrypted = Buffer.concat([decrypted, decipher.final()])
return decrypted.toString()
}
async deleteByID(_id) {
if (!_id) {
throw new Error("Missing _id")
}
const entry = await this.model.findById(_id).catch(() => null)
if (!entry) {
return null
}
await entry.delete()
return entry
}
}

View File

@ -0,0 +1,6 @@
export default {
withAuthentication: require("./withAuthentication").default,
withOptionalAuthentication: require("./withOptionalAuthentication").default,
onlyAdmin: require("./onlyAdmin").default,
roles: require("./roles").default,
}

View File

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

View File

@ -0,0 +1,19 @@
export default (req, res, next) => {
req.isAdmin = () => {
if (req.user.roles.includes("admin")) {
return true
}
return false
}
req.hasRole = (role) => {
if (req.user.roles.includes(role)) {
return true
}
return false
}
next()
}

View File

@ -0,0 +1,78 @@
import { authorizedServerTokens } from "../../classes/DbModels"
import SecureEntry from "../../classes/SecureEntry"
import AuthToken from "../../classes/AuthToken"
export default async (req, res, next) => {
function reject(description) {
return res.status(401).json({ error: `${description ?? "Invalid session"}` })
}
try {
const tokenAuthHeader = req.headers?.authorization?.split(" ")
if (!tokenAuthHeader) {
return reject("Missing token header")
}
if (!tokenAuthHeader[1]) {
return reject("Recived header, missing token")
}
switch (tokenAuthHeader[0]) {
case "Bearer": {
const token = tokenAuthHeader[1]
const validation = await AuthToken.validate(token)
if (!validation.valid) {
return reject(validation.error)
}
req.auth = {
token: token,
decoded: validation.data,
session: validation.session,
user: validation.user
}
return next()
}
case "Server": {
const [client_id, token] = tokenAuthHeader[1].split(":")
if (client_id === "undefined" || token === "undefined") {
return reject("Invalid server token")
}
const secureEntries = new SecureEntry(authorizedServerTokens)
const serverTokenEntry = await secureEntries.get(client_id, undefined, {
keyName: "client_id",
valueName: "token",
})
if (!serverTokenEntry) {
return reject("Invalid server token")
}
if (serverTokenEntry !== token) {
return reject("Missmatching server token")
}
req.user = {
__server: true,
_id: client_id,
roles: ["server"],
}
return next()
}
default: {
return reject("Invalid token type")
}
}
} catch (error) {
console.error(error)
return res.status(500).json({ error: "An error occurred meanwhile authenticating your token" })
}
}

View File

@ -0,0 +1,9 @@
import withAuthentication from "../withAuthentication"
export default (req, res, next) => {
if (req.headers?.authorization) {
withAuthentication(req, res, next)
} else {
next()
}
}

View File

@ -23,6 +23,7 @@
"dotenv": "^16.4.4",
"http-proxy-middleware": "^2.0.6",
"hyper-express": "^6.14.12",
"jsonwebtoken": "^9.0.2",
"linebridge": "^0.16.0",
"module-alias": "^2.2.3",
"p-map": "^4.0.0",

View File

@ -0,0 +1,25 @@
import { Server } from "linebridge/src/server"
import DbManager from "@shared-classes/DbManager"
import SharedMiddlewares from "@shared-middlewares"
export default class API extends Server {
static refName = "auth"
static useEngine = "hyper-express"
static routesPath = `${__dirname}/routes`
static listen_port = process.env.HTTP_LISTEN_PORT ?? 3020
middlewares = {
...SharedMiddlewares
}
contexts = {
db: new DbManager(),
}
async onInitialize() {
await this.contexts.db.initialize()
}
}
Boot(API)

View File

@ -0,0 +1,6 @@
{
"name": "auth",
"version": "1.0.0",
"main": "index.js",
"license": "MIT"
}

View File

@ -0,0 +1,22 @@
import { Session } from "@shared-classes/DbModels"
export default {
middlewares: ["withAuthentication"],
fn: async (req) => {
const { token, session } = req.auth
const deletedSession = await Session.findOneAndDelete({
user_id: session.user_id,
token: token,
})
if (session) {
return {
message: "Session deleted",
session: deletedSession
}
}
throw new OperationError(404, "Session not found")
}
}

View File

@ -0,0 +1,36 @@
import AuthToken from "@shared-classes/AuthToken"
import { User } from "@shared-classes/DbModels"
import requiredFields from "@shared-utils/requiredFields"
import bcrypt from "bcrypt"
export default async (req, res) => {
requiredFields(["username", "password"], req.body)
const { username, password } = req.body
let isEmail = username.match(/^[^\s@]+@[^\s@]+\.[^\s@]+$/)
let query = isEmail ? { email: username } : { username: username }
const user = await User.findOne(query).select("+password")
if (!user) {
throw new OperationError(401, "User not found")
}
if (!bcrypt.compareSync(password, user.password)) {
return res.status(401).json({
message: "Invalid credentials",
})
}
const token = await AuthToken.createAuth({
username: user.username,
user_id: user._id.toString(),
ip_address: req.headers["x-forwarded-for"]?.split(",")[0] ?? req.socket?.remoteAddress ?? req.ip,
client: req.headers["user-agent"],
//signLocation: global.signLocation,
})
return { token: token }
}

View File

@ -0,0 +1,30 @@
import { User } from "@shared-classes/DbModels"
export default async (req) => {
const { username, email } = req.query
if (!username && !email) {
throw new OperationError(400, "Missing username or email")
}
const user = await User.findOne({
$or: [
{ username: username },
{ email: email },
]
}).catch((error) => {
return false
})
if (user) {
return {
message: "User already exists",
exists: true,
}
} else {
return {
message: "User doesn't exists",
exists: false,
}
}
}

View File

@ -0,0 +1,72 @@
import { User } from "@shared-classes/DbModels"
import bcrypt from "bcrypt"
import requiredFields from "@shared-utils/requiredFields"
export default async (req) => {
requiredFields(["username", "password", "email"], req.body)
let { username, password, email, fullName, roles, avatar, acceptTos } = req.body
if (ToBoolean(acceptTos) !== true) {
throw new OperationError(400, "You must accept the terms of service in order to create an account.")
}
if (username.length < 3) {
throw new OperationError(400, "Username must be at least 3 characters")
}
if (username.length > 64) {
throw new OperationError(400, "Username cannot be longer than 64 characters")
}
// if username has capital letters, throw error
if (username !== username.toLowerCase()) {
throw new OperationError(400, "Username must be lowercase")
}
// make sure the username has no spaces
if (username.includes(" ")) {
throw new OperationError(400, "Username cannot contain spaces")
}
// make sure the username has no valid characters. Only letters, numbers, and underscores
if (!/^[a-z0-9_]+$/.test(username)) {
throw new OperationError(400, "Username can only contain letters, numbers, and underscores")
}
// check if username is already taken
const existentUser = await User.findOne({ username: username })
if (existentUser) {
throw new OperationError(400, "User already exists")
}
// check if the email is already in use
const existentEmail = await User.findOne({ email: email })
if (existentEmail) {
throw new OperationError(400, "Email already in use")
}
// hash the password
const hash = bcrypt.hashSync(password, parseInt(process.env.BCRYPT_ROUNDS ?? 3))
let user = new User({
username: username,
password: hash,
email: email,
fullName: fullName,
avatar: avatar ?? `https://api.dicebear.com/7.x/thumbs/svg?seed=${username}`,
roles: roles,
createdAt: new Date().getTime(),
acceptTos: acceptTos,
})
await user.save()
// TODO: dispatch event bus
//global.eventBus.emit("user.create", user)
return user
}

View File

@ -21,7 +21,7 @@ export default class API {
this.options = {
listenHost: process.env.HTTP_LISTEN_HOST || "0.0.0.0",
listenPort: process.env.HTTP_LISTEN_PORT || 3020,
listenPort: process.env.HTTP_LISTEN_PORT || 3004,
...options
}
}

View File

@ -37,7 +37,7 @@ export default class FileServerAPI {
server = global.server = express()
listenIp = process.env.HTTP_LISTEN_IP ?? "0.0.0.0"
listenPort = process.env.HTTP_LISTEN_PORT ?? 3060
listenPort = process.env.HTTP_LISTEN_PORT ?? 3002
redis = global.redis = RedisClient()

View File

@ -8,7 +8,7 @@ import Token from "@lib/token"
export default class API extends Server {
static refName = "MAIN-API"
static listen_port = process.env.HTTP_LISTEN_PORT || 3010
static listen_port = process.env.HTTP_LISTEN_PORT || 3000
static requireWSAuth = true
constructor(params) {

View File

@ -28,7 +28,6 @@
"nsfwjs": "^3.0.0",
"p-map": "^4.0.0",
"p-queue": "^7.3.4",
"path-to-regexp": "^6.2.1",
"sharp": "^0.33.2"
}
}

View File

@ -16,7 +16,7 @@ export default class API {
server = global.server = new hyperexpress.Server()
listenIp = process.env.HTTP_LISTEN_IP ?? "0.0.0.0"
listenPort = process.env.HTTP_LISTEN_PORT ?? 3040
listenPort = process.env.HTTP_LISTEN_PORT ?? 3005
internalRouter = new hyperexpress.Router()

View File

@ -28,7 +28,7 @@ export default class API {
this.options = {
listenHost: process.env.HTTP_LISTEN_IP ?? "0.0.0.0",
listenPort: process.env.HTTP_LISTEN_PORT ?? 3050,
listenPort: process.env.HTTP_LISTEN_PORT ?? 3003,
...options
}
}

View File

@ -10,8 +10,6 @@ export default async (payload) => {
posts = [posts]
}
console.log(posts, posts.every((post) => !post))
if (posts.every((post) => !post)) {
return []
}

View File

@ -1,6 +1,9 @@
import { Server } from "linebridge/src/server"
import DbManager from "@shared-classes/DbManager"
import RedisClient from "@shared-classes/RedisClient"
import SharedMiddlewares from "@shared-middlewares"
export default class API extends Server {
static refName = "posts"
@ -8,8 +11,13 @@ export default class API extends Server {
static routesPath = `${__dirname}/routes`
static listen_port = process.env.HTTP_LISTEN_PORT ?? 3001
middlewares = {
...SharedMiddlewares
}
contexts = {
db: new DbManager(),
redis: RedisClient()
}
events = {
@ -18,6 +26,7 @@ export default class API extends Server {
async onInitialize() {
await this.contexts.db.initialize()
await this.contexts.redis.initialize()
}
}

View File

@ -3,9 +3,9 @@ import Posts from "@classes/posts"
export default {
middlewares: ["withOptionalAuthentication"],
fn: async (req, res) => {
const result = await Posts.data({
const result = await Posts.data({
post_id: req.params.post_id,
for_user_id: req.user?._id.toString(),
for_user_id: req.auth?.session?.user_id,
})
return result

View File

@ -15,7 +15,7 @@ export default class API {
server = global.server = new hyperexpress.Server()
listenIp = process.env.HTTP_LISTEN_IP ?? "0.0.0.0"
listenPort = process.env.HTTP_LISTEN_PORT ?? 3070
listenPort = process.env.HTTP_LISTEN_PORT ?? 3006
internalRouter = new hyperexpress.Router()

View File

@ -17,6 +17,6 @@ export default (fields, obj) => {
}
if (missing.length > 0) {
throw new Error(`Missing required fields: ${missing.join(", ")}`)
throw new OperationError(400, `Missing required fields: ${missing.join(", ")}`)
}
}