Refactor users service routes and add websocket support

- Standardize middleware naming to useMiddlewares - Add websocket
support and handleWsUpgrade to users service - Update getFollowers to
support pagination and return metadata - Emit user update events via
websockets - Add linebridge^1.0.0 dependency - Minor code style and
consistency improvements across routes
This commit is contained in:
srgooglo 2025-07-04 14:16:21 +02:00
parent 35df3a421f
commit 33fe2044c4
19 changed files with 245 additions and 189 deletions

View File

@ -1,35 +1,44 @@
import { User, UserFollow } from "@db_models"
export default async (payload = {}) => {
const { user_id, data = false, limit = 50, offset = 0 } = payload
const { user_id, data = false, limit = 50, page = 0 } = payload
if (!user_id) {
throw new OperationError(400, "Missing user_id")
}
if (data) {
const total_followers = await UserFollow.countDocuments({
to: user_id,
})
if (data === true) {
let followers = await UserFollow.find({
to: user_id,
})
.limit(limit)
.skip(offset)
.skip(limit * page)
.lean()
followers = followers.map((follow) => {
return follow.user_id
})
const followersData = await User.find({
_id: {
$in: followers.map((follow) => {
return follow.user_id
}),
$in: followers,
},
})
return followersData
} else {
const count = await UserFollow.countDocuments({
to: user_id,
})
const nextPage = page + 1
return {
count,
items: followersData,
total_items: total_followers,
has_more: total_followers > limit * nextPage,
}
} else {
return {
count: total_followers,
}
}
}

View File

@ -25,5 +25,13 @@ export default async (user_id, update) => {
user = user.toObject()
const userSockets = await global.websockets.find.clientsByUserId(
user._id.toString(),
)
for (const userSocket of userSockets) {
userSocket.emit(`self:user:update`, user)
}
return user
}

View File

@ -1,3 +1,6 @@
{
"name": "users"
"name": "users",
"dependencies": {
"linebridge": "^1.0.0-alpha.4"
}
}

View File

@ -5,17 +5,17 @@ export default {
const { user_id } = req.params
const user = await User.findOne({
_id: user_id
_id: user_id,
}).catch((err) => {
return false
})
const badges = await Badge.find({
name: {
$in: user.badges
}
$in: user.badges,
},
})
return badges
}
},
}

View File

@ -1,7 +1,7 @@
import Users from "@classes/users"
export default {
middlewares: ["withOptionalAuthentication"],
useMiddlewares: ["withOptionalAuthentication"],
fn: async (req) => {
const { user_id } = req.params
@ -10,7 +10,7 @@ export default {
return await Users.data({
user_id: ids.length > 1 ? ids : user_id,
from_user_id: req.auth?.session.user_id,
basic: req.query?.basic,
basic: ToBoolean(req.query?.basic),
})
},
}

View File

@ -1,12 +1,12 @@
import User from "@classes/users"
export default {
middlewares: ["withAuthentication"],
useMiddlewares: ["withAuthentication"],
fn: async (req) => {
return await User.toggleFollow({
user_id: req.params.user_id,
from_user_id: req.auth.session.user_id,
to: req.body?.to,
})
}
},
}

View File

@ -5,8 +5,8 @@ export default {
return await User.getFollowers({
user_id: req.params.user_id,
data: ToBoolean(req.query.fetchData),
limit: req.query.limit,
offset: req.query.offset,
limit: parseInt(req.query.limit),
page: parseInt(req.query.page),
})
}
},
}

View File

@ -1,7 +1,7 @@
import { UserPublicKey } from "@db_models"
export default {
middlewares: ["withAuthentication"],
useMiddlewares: ["withAuthentication"],
fn: async (req) => {
const targetUserId = req.params.user_id

View File

@ -1,12 +1,12 @@
import Users from "@classes/users"
export default {
middlewares: ["withOptionalAuthentication"],
useMiddlewares: ["withOptionalAuthentication"],
fn: async (req) => {
const data = await Users.data({
user_id: req.auth.session.user_id,
})
return data.roles
}
},
}

View File

@ -1,18 +1,18 @@
import { UserConfig } from "@db_models"
export default {
middlewares: ["withAuthentication"],
useMiddlewares: ["withAuthentication"],
fn: async (req) => {
const key = req.query.key
let config = await UserConfig.findOne({
user_id: req.auth.session.user_id
user_id: req.auth.session.user_id,
})
if (!config) {
config = await UserConfig.create({
user_id: req.auth.session.user_id,
values: {}
values: {},
})
}
@ -21,5 +21,5 @@ export default {
}
return config.values
}
},
}

View File

@ -5,20 +5,20 @@ const baseConfig = [
{
key: "app:language",
type: "string",
value: "en-us"
value: "en-us",
},
{
key: "auth:mfa",
type: "boolean",
value: false
value: false,
},
]
export default {
middlewares: ["withAuthentication"],
useMiddlewares: ["withAuthentication"],
fn: async (req) => {
let config = await UserConfig.findOne({
user_id: req.auth.session.user_id
user_id: req.auth.session.user_id,
})
const values = {}
@ -29,33 +29,38 @@ export default {
if (typeof fromBody === config.type) {
values[config.key] = req.body[config.key]
} else {
throw new OperationError(400, `Invalid type for ${config.key}`)
throw new OperationError(
400,
`Invalid type for ${config.key}`,
)
}
} else {
values[config.key] = config.value
}
})
if (!config) {
config = await UserConfig.create({
user_id: req.auth.session.user_id,
values
values,
})
} else {
const newValues = lodash.merge(config.values, values)
config = await UserConfig.updateOne({
user_id: req.auth.session.user_id
}, {
values: newValues
})
config = await UserConfig.updateOne(
{
user_id: req.auth.session.user_id,
},
{
values: newValues,
},
)
config = await UserConfig.findOne({
user_id: req.auth.session.user_id
user_id: req.auth.session.user_id,
})
}
return config.values
}
},
}

View File

@ -1,7 +1,7 @@
import Users from "@classes/users"
export default {
middlewares: ["withAuthentication"],
useMiddlewares: ["withAuthentication"],
fn: async (req) => {
return await Users.data({
user_id: req.auth.session.user_id,

View File

@ -1,7 +1,7 @@
import { UserDHKeyPair } from "@db_models"
export default {
middlewares: ["withAuthentication"],
useMiddlewares: ["withAuthentication"],
fn: async (req) => {
const userId = req.auth.session.user_id

View File

@ -1,7 +1,7 @@
import { UserDHKeyPair } from "@db_models"
export default {
middlewares: ["withAuthentication"],
useMiddlewares: ["withAuthentication"],
fn: async (req) => {
const userId = req.auth.session.user_id
const { str } = req.body

View File

@ -1,7 +1,7 @@
import { UserPublicKey } from "@db_models"
export default {
middlewares: ["withAuthentication"],
useMiddlewares: ["withAuthentication"],
fn: async (req) => {
const userId = req.auth.session.user_id
const { public_key } = req.body

View File

@ -1,7 +1,7 @@
import Users from "@classes/users"
export default {
middlewares: ["withAuthentication"],
useMiddlewares: ["withAuthentication"],
fn: async (req) => {
const data = await Users.data({
user_id: user_id,
@ -12,5 +12,5 @@ export default {
}
return data.roles
}
},
}

View File

@ -19,7 +19,7 @@ const MaxStringsLengths = {
}
export default {
middlewares: ["withAuthentication"],
useMiddlewares: ["withAuthentication"],
fn: async (req) => {
const update = {}
@ -27,10 +27,16 @@ export default {
AllowedPublicUpdateFields.forEach((key) => {
if (typeof req.body[key] !== "undefined") {
// check maximung strings length
if (typeof req.body[key] === "string" && MaxStringsLengths[key]) {
if (
typeof req.body[key] === "string" &&
MaxStringsLengths[key]
) {
if (req.body[key].length > MaxStringsLengths[key]) {
// create a substring
update[key] = req.body[key].substring(0, MaxStringsLengths[key])
update[key] = req.body[key].substring(
0,
MaxStringsLengths[key],
)
} else {
update[key] = req.body[key]
}
@ -53,5 +59,5 @@ export default {
}
return await UserClass.update(req.auth.session.user_id, update)
}
},
}

View File

@ -4,12 +4,19 @@ import DbManager from "@shared-classes/DbManager"
import RedisClient from "@shared-classes/RedisClient"
import SharedMiddlewares from "@shared-middlewares"
import InjectedAuth from "@shared-lib/injectedAuth"
export default class API extends Server {
static refName = "users"
static useEngine = "hyper-express"
static routesPath = `${__dirname}/routes`
static listen_port = process.env.HTTP_LISTEN_PORT ?? 3008
static listenPort = process.env.HTTP_LISTEN_PORT ?? 3008
static useMiddlewares = ["logs"]
static websockets = {
enabled: true,
path: "/users",
}
middlewares = {
...SharedMiddlewares,
@ -20,6 +27,24 @@ export default class API extends Server {
redis: RedisClient(),
}
handleWsUpgrade = async (context, token, res) => {
if (!token) {
return res.upgrade(context)
}
context = await InjectedAuth(context, token, res).catch(() => {
res.close(401, "Failed to verify auth token")
return false
})
if (!context || !context.user) {
res.close(401, "Unauthorized or missing auth token")
return false
}
return res.upgrade(context)
}
async onInitialize() {
await this.contexts.db.initialize()
await this.contexts.redis.initialize()