diff --git a/packages/server/src/controllers/AuthController/endpoints/userLogin.js b/packages/server/src/controllers/AuthController/endpoints/userLogin.js new file mode 100644 index 00000000..bdc6dcf9 --- /dev/null +++ b/packages/server/src/controllers/AuthController/endpoints/userLogin.js @@ -0,0 +1,22 @@ +import passport from "passport" +import { Token } from "@lib" + +export default { + method: "POST", + route: "/login", + fn: 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) + } +} \ No newline at end of file diff --git a/packages/server/src/controllers/AuthController/endpoints/userLogout.js b/packages/server/src/controllers/AuthController/endpoints/userLogout.js new file mode 100644 index 00000000..b4f22252 --- /dev/null +++ b/packages/server/src/controllers/AuthController/endpoints/userLogout.js @@ -0,0 +1,25 @@ +import { Session } from "@models" + +export default { + method: "POST", + route: "/logout", + middlewares: ["withAuthentication"], + fn: async (req, res) => { + const { token, user_id } = req.body + + if (typeof user_id === "undefined") { + return res.status(400).json("No user_id provided") + } + if (typeof token === "undefined") { + return res.status(400).json("No token provided") + } + + const session = await Session.findOneAndDelete({ user_id, token }) + + if (session) { + return res.json("done") + } + + return res.status(404).json("not found") + }, +} \ No newline at end of file diff --git a/packages/server/src/controllers/AuthController/endpoints/userRegister.js b/packages/server/src/controllers/AuthController/endpoints/userRegister.js new file mode 100644 index 00000000..c54c33f5 --- /dev/null +++ b/packages/server/src/controllers/AuthController/endpoints/userRegister.js @@ -0,0 +1,18 @@ +import { Schematized } from "@lib" + +import createUser from "../methods/createUser" + +export default { + method: "POST", + route: "/register", + fn: Schematized({ + required: ["username", "email", "password"], + select: ["username", "email", "password", "fullName"], + }, async (req, res) => { + const result = await createUser(req.selection).catch((err) => { + return res.status(500).json(err.message) + }) + + return res.json(result) + }) +} \ No newline at end of file diff --git a/packages/server/src/controllers/AuthController/index.js b/packages/server/src/controllers/AuthController/index.js new file mode 100644 index 00000000..87c2d19e --- /dev/null +++ b/packages/server/src/controllers/AuthController/index.js @@ -0,0 +1,9 @@ +import { Controller } from "linebridge/dist/server" +import generateEndpointsFromDir from "linebridge/dist/server/lib/generateEndpointsFromDir" + +export default class AuthController extends Controller { + static refName = "AuthController" + static useRoute = "/auth" + + httpEndpoints = generateEndpointsFromDir(__dirname + "/endpoints") +} \ No newline at end of file diff --git a/packages/server/src/controllers/AuthController/methods/createUser.js b/packages/server/src/controllers/AuthController/methods/createUser.js new file mode 100755 index 00000000..cd5f3fe7 --- /dev/null +++ b/packages/server/src/controllers/AuthController/methods/createUser.js @@ -0,0 +1,54 @@ +import { User } from "@models" +import Avatars from "dicebar_lib" +import bcrypt from "bcrypt" + +export default async function (payload) { + let { username, password, email, fullName, roles, avatar } = payload + + // if username has capital letters, throw error + if (username !== username.toLowerCase()) { + throw new Error("Username must be lowercase") + } + + // make sure the username has no spaces + if (username.includes(" ")) { + throw new Error("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 Error("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 Error("User already exists") + } + + // check if the email is already in use + const existentEmail = await User.findOne({ email: email }) + + if (existentEmail) { + throw new Error("Email already in use") + } + + // hash the password + const hash = bcrypt.hashSync(password, parseInt(process.env.BCRYPT_ROUNDS ?? 3)) + + // create the doc + let user = new User({ + username: username, + password: hash, + email: email, + fullName: fullName, + avatar: avatar ?? Avatars.generate({ seed: username, type: "initials" }).uri, + roles: roles, + createdAt: new Date().getTime(), + }) + + await user.save() + + return user +} \ No newline at end of file diff --git a/packages/server/src/controllers/BadgesController/endpoints/deleteBadge.js b/packages/server/src/controllers/BadgesController/endpoints/deleteBadge.js new file mode 100644 index 00000000..69150835 --- /dev/null +++ b/packages/server/src/controllers/BadgesController/endpoints/deleteBadge.js @@ -0,0 +1,21 @@ +import { Badge } from "@models" + +export default { + method: "DELETE", + route: "/badge/:badge_id", + middlewares: ["withAuthentication", "onlyAdmin"], + fn: async (req, res) => { + const badge = await Badge.findById(req.params.badge_id).catch((err) => { + res.status(500).json({ error: err }) + return false + }) + + if (!badge) { + return res.status(404).json({ error: "No badge founded" }) + } + + badge.remove() + + return res.json(badge) + } +} \ No newline at end of file diff --git a/packages/server/src/controllers/BadgesController/endpoints/getBadges.js b/packages/server/src/controllers/BadgesController/endpoints/getBadges.js new file mode 100644 index 00000000..51bff0a1 --- /dev/null +++ b/packages/server/src/controllers/BadgesController/endpoints/getBadges.js @@ -0,0 +1,29 @@ +import { Schematized } from "@lib" +import { Badge } from "@models" + +export default { + method: "GET", + route: "/", + fn: Schematized({ + select: ["_id", "name", "label"], + }, async (req, res) => { + let badges = [] + + if (req.selection._id) { + badges = await Badge.find({ + _id: { $in: req.selection._id }, + }) + + badges = badges.map(badge => badge.toObject()) + } else { + badges = await Badge.find(req.selection).catch((err) => { + res.status(500).json({ error: err }) + return false + }) + } + + if (badges) { + return res.json(badges) + } + }) +} \ No newline at end of file diff --git a/packages/server/src/controllers/BadgesController/endpoints/getUserBadges.js b/packages/server/src/controllers/BadgesController/endpoints/getUserBadges.js new file mode 100644 index 00000000..3065171f --- /dev/null +++ b/packages/server/src/controllers/BadgesController/endpoints/getUserBadges.js @@ -0,0 +1,19 @@ +import { User, Badge } from "@models" + +export default { + method: "GET", + route: "/user", + fn: async (req, res) => { + const user = await User.findOne({ _id: req.query.user_id ?? req.user._id }) + + if (!user) { + return res.status(404).json({ error: "User not found" }) + } + + const badges = await Badge.find({ + name: { $in: user.badges }, + }) + + return res.json(badges) + } +} \ No newline at end of file diff --git a/packages/server/src/controllers/BadgesController/endpoints/giveToUser.js b/packages/server/src/controllers/BadgesController/endpoints/giveToUser.js new file mode 100644 index 00000000..3a171652 --- /dev/null +++ b/packages/server/src/controllers/BadgesController/endpoints/giveToUser.js @@ -0,0 +1,41 @@ +import { Badge, User } from "@models" +import { Schematized } from "@lib" + +export default { + method: "POST", + route: "/badge/:badge_id/giveToUser", + middlewares: ["withAuthentication", "onlyAdmin"], + fn: Schematized({ + required: ["user_id"], + select: ["user_id"], + }, async (req, res) => { + const badge = await Badge.findById(req.params.badge_id).catch((err) => { + res.status(500).json({ error: err }) + return false + }) + + if (!badge) { + return res.status(404).json({ error: "No badge founded" }) + } + + const user = await User.findById(req.selection.user_id).catch((err) => { + res.status(500).json({ error: err }) + return false + }) + + if (!user) { + return res.status(404).json({ error: "No user founded" }) + } + + // check if user already have this badge + if (user.badges.includes(badge._id)) { + return res.status(409).json({ error: "User already have this badge" }) + } + + user.badges.push(badge._id.toString()) + + user.save() + + return res.json(user) + }) +} \ No newline at end of file diff --git a/packages/server/src/controllers/BadgesController/endpoints/putBadge.js b/packages/server/src/controllers/BadgesController/endpoints/putBadge.js new file mode 100644 index 00000000..be5bc499 --- /dev/null +++ b/packages/server/src/controllers/BadgesController/endpoints/putBadge.js @@ -0,0 +1,27 @@ +import { Badge } from "@models" +import { Schematized } from "@lib" + +export default { + method: "PUT", + route: "/", + middlewares: ["withAuthentication", "onlyAdmin"], + fn: Schematized({ + select: ["badge_id", "name", "label", "description", "icon", "color"], + }, async (req, res) => { + let badge = await Badge.findById(req.selection.badge_id).catch((err) => null) + + if (!badge) { + badge = new Badge() + } + + badge.name = req.selection.name || badge.name + badge.label = req.selection.label || badge.label + badge.description = req.selection.description || badge.description + badge.icon = req.selection.icon || badge.icon + badge.color = req.selection.color || badge.color + + badge.save() + + return res.json(badge) + }) +} \ No newline at end of file diff --git a/packages/server/src/controllers/BadgesController/endpoints/removeToUser.js b/packages/server/src/controllers/BadgesController/endpoints/removeToUser.js new file mode 100644 index 00000000..1f35ffa4 --- /dev/null +++ b/packages/server/src/controllers/BadgesController/endpoints/removeToUser.js @@ -0,0 +1,41 @@ +import { Schematized } from "@lib" +import { Badge, User } from "@models" + +export default { + method: "DELETE", + route: "/badge/:badge_id/removeFromUser", + middlewares: ["withAuthentication", "onlyAdmin"], + fn: Schematized({ + required: ["user_id"], + select: ["user_id"], + }, async (req, res) => { + const badge = await Badge.findById(req.params.badge_id).catch((err) => { + res.status(500).json({ error: err }) + return false + }) + + if (!badge) { + return res.status(404).json({ error: "No badge founded" }) + } + + const user = await User.findById(req.selection.user_id).catch((err) => { + res.status(500).json({ error: err }) + return false + }) + + if (!user) { + return res.status(404).json({ error: "No user founded" }) + } + + // check if user already have this badge + if (!user.badges.includes(badge._id)) { + return res.status(409).json({ error: "User don't have this badge" }) + } + + user.badges = user.badges.filter(b => b !== badge._id.toString()) + + user.save() + + return res.json(user) + }) +} \ No newline at end of file diff --git a/packages/server/src/controllers/BadgesController/index.js b/packages/server/src/controllers/BadgesController/index.js index 20f737ac..3ce58f83 100755 --- a/packages/server/src/controllers/BadgesController/index.js +++ b/packages/server/src/controllers/BadgesController/index.js @@ -1,198 +1,9 @@ import { Controller } from "linebridge/dist/server" -import { Badge, User } from "../../models" -import { Schematized } from "../../lib" +import generateEndpointsFromDir from "linebridge/dist/server/lib/generateEndpointsFromDir" export default class BadgesController extends Controller { static refName = "BadgesController" + static useRoute = "/badge" - get = { - "/badges": Schematized({ - select: ["_id", "name", "label"], - }, async (req, res) => { - let badges = [] - - if (req.selection._id) { - badges = await Badge.find({ - _id: { $in: req.selection._id }, - }) - - badges = badges.map(badge => badge.toObject()) - } else { - badges = await Badge.find(req.selection).catch((err) => { - res.status(500).json({ error: err }) - return false - }) - } - - if (badges) { - return res.json(badges) - } - }), - "/user/badges": { - middlewares: ["withAuthentication"], - fn: async (req, res) => { - const user = await User.findOne({ _id: req.query.user_id ?? req.user._id }) - - if (!user) { - return res.status(404).json({ error: "User not found" }) - } - - const badges = await Badge.find({ - name: { $in: user.badges }, - }) - - return res.json(badges) - } - } - } - - post = { - "/badge": { - middlewares: ["withAuthentication"], - fn: Schematized({ - required: ["name"], - select: ["name", "label", "description", "icon", "color"], - }, async (req, res) => { - await Badge.findOne(req.selection).then((data) => { - if (data) { - return res.status(409).json({ - error: "This badge is already created", - }) - } - - let badge = new Badge({ - name: req.selection.name, - label: req.selection.label, - description: req.selection.description, - icon: req.selection.icon, - color: req.selection.color, - }) - - badge.save() - - return res.json(badge) - }) - }) - }, - "/badge/:badge_id/giveToUser": { - middlewares: ["withAuthentication", "onlyAdmin"], - fn: Schematized({ - required: ["user_id"], - select: ["user_id"], - }, async (req, res) => { - const badge = await Badge.findById(req.params.badge_id).catch((err) => { - res.status(500).json({ error: err }) - return false - }) - - if (!badge) { - return res.status(404).json({ error: "No badge founded" }) - } - - const user = await User.findById(req.selection.user_id).catch((err) => { - res.status(500).json({ error: err }) - return false - }) - - if (!user) { - return res.status(404).json({ error: "No user founded" }) - } - - // check if user already have this badge - if (user.badges.includes(badge._id)) { - return res.status(409).json({ error: "User already have this badge" }) - } - - user.badges.push(badge._id.toString()) - - user.save() - - return res.json(user) - }) - } - } - - put = { - "/badge/:badge_id": { - middlewares: ["withAuthentication", "onlyAdmin"], - fn: Schematized({ - select: ["name", "label", "description", "icon", "color"], - }, async (req, res) => { - const badge = await Badge.findById(req.params.badge_id).catch((err) => { - res.status(500).json({ error: err }) - return false - }) - - if (!badge) { - return res.status(404).json({ error: "No badge founded" }) - } - - badge.name = req.selection.name || badge.name - badge.label = req.selection.label || badge.label - badge.description = req.selection.description || badge.description - badge.icon = req.selection.icon || badge.icon - badge.color = req.selection.color || badge.color - - badge.save() - - return res.json(badge) - }) - } - } - - delete = { - "/badge/:badge_id": { - middlewares: ["withAuthentication", "onlyAdmin"], - fn: async (req, res) => { - const badge = await Badge.findById(req.params.badge_id).catch((err) => { - res.status(500).json({ error: err }) - return false - }) - - if (!badge) { - return res.status(404).json({ error: "No badge founded" }) - } - - badge.remove() - - return res.json(badge) - } - }, - "/badge/:badge_id/removeFromUser": { - middlewares: ["withAuthentication", "onlyAdmin"], - fn: Schematized({ - required: ["user_id"], - select: ["user_id"], - }, async (req, res) => { - const badge = await Badge.findById(req.params.badge_id).catch((err) => { - res.status(500).json({ error: err }) - return false - }) - - if (!badge) { - return res.status(404).json({ error: "No badge founded" }) - } - - const user = await User.findById(req.selection.user_id).catch((err) => { - res.status(500).json({ error: err }) - return false - }) - - if (!user) { - return res.status(404).json({ error: "No user founded" }) - } - - // check if user already have this badge - if (!user.badges.includes(badge._id)) { - return res.status(409).json({ error: "User don't have this badge" }) - } - - user.badges = user.badges.filter(b => b !== badge._id.toString()) - - user.save() - - return res.json(user) - }) - } - } + httpEndpoints = generateEndpointsFromDir(__dirname + "/endpoints") } \ No newline at end of file diff --git a/packages/server/src/controllers/CommentsController/endpoints/createPostComment.js b/packages/server/src/controllers/CommentsController/endpoints/createPostComment.js new file mode 100644 index 00000000..82b41aa8 --- /dev/null +++ b/packages/server/src/controllers/CommentsController/endpoints/createPostComment.js @@ -0,0 +1,28 @@ +import { Schematized } from "@lib" +import newComment from "../methods/newComment" + +export default { + method: "POST", + route: "/post/:post_id", + fn: Schematized({ + required: ["message"], + select: ["message"], + }, async (req, res) => { + const { post_id } = req.params + const { message } = req.selection + + try { + const comment = newComment({ + user_id: req.user._id.toString(), + parent_id: post_id, + message: message, + }) + + return res.json(comment) + } catch (error) { + return res.status(400).json({ + error: error.message, + }) + } + }) +} \ No newline at end of file diff --git a/packages/server/src/controllers/CommentsController/endpoints/deletePostComment.js b/packages/server/src/controllers/CommentsController/endpoints/deletePostComment.js new file mode 100644 index 00000000..c87c3ca3 --- /dev/null +++ b/packages/server/src/controllers/CommentsController/endpoints/deletePostComment.js @@ -0,0 +1,21 @@ +import deleteComment from "../methods/deleteComment" + +export default { + method: "DELETE", + route: "/post/:post_id/:comment_id", + middlewares: ["withAuthentication"], + fn: async (req, res) => { + const result = await deleteComment({ + comment_id: req.params.comment_id, + issuer_id: req.user._id.toString(), + }).catch((err) => { + res.status(500).json({ message: err.message }) + + return false + }) + + if (result) { + return res.json(result) + } + } +} \ No newline at end of file diff --git a/packages/server/src/controllers/CommentsController/endpoints/getPostComments.js b/packages/server/src/controllers/CommentsController/endpoints/getPostComments.js new file mode 100644 index 00000000..0f3ae4e6 --- /dev/null +++ b/packages/server/src/controllers/CommentsController/endpoints/getPostComments.js @@ -0,0 +1,21 @@ +import getComments from "../methods/getComments" + +export default { + method: "GET", + route: "/post/:post_id", + fn: async (req, res) => { + const { post_id } = req.params + + const comments = await getComments({ parent_id: post_id }).catch((err) => { + res.status(400).json({ + error: err.message, + }) + + return false + }) + + if (!comments) return + + return res.json(comments) + } +} \ No newline at end of file diff --git a/packages/server/src/controllers/CommentsController/index.js b/packages/server/src/controllers/CommentsController/index.js index 3907f732..644a9704 100755 --- a/packages/server/src/controllers/CommentsController/index.js +++ b/packages/server/src/controllers/CommentsController/index.js @@ -1,77 +1,9 @@ import { Controller } from "linebridge/dist/server" -import { Schematized } from "../../lib" - -import getComments from "./methods/getComments" -import newComment from "./methods/newComment" -import deleteComment from "./methods/deleteComment" +import generateEndpointsFromDir from "linebridge/dist/server/lib/generateEndpointsFromDir" export default class CommentsController extends Controller { static refName = "CommentsController" + static useRoute = "/comments" - get = { - "/posts/:post_id/comments": { - fn: async (req, res) => { - const { post_id } = req.params - - const comments = await getComments({ parent_id: post_id }).catch((err) => { - res.status(400).json({ - error: err.message, - }) - - return false - }) - - if (!comments) return - - return res.json(comments) - } - } - } - - post = { - "/posts/:post_id/comment": { - middlewares: ["withAuthentication"], - fn: Schematized({ - required: ["message"], - select: ["message"], - }, async (req, res) => { - const { post_id } = req.params - const { message } = req.selection - - try { - const comment = newComment({ - user_id: req.user._id.toString(), - parent_id: post_id, - message: message, - }) - - return res.json(comment) - } catch (error) { - return res.status(400).json({ - error: error.message, - }) - } - }) - } - } - - delete = { - "/posts/:post_id/comment/:comment_id": { - middlewares: ["withAuthentication"], - fn: async (req, res) => { - const result = await deleteComment({ - comment_id: req.params.comment_id, - issuer_id: req.user._id.toString(), - }).catch((err) => { - res.status(500).json({ message: err.message }) - - return false - }) - - if (result) { - return res.json(result) - } - } - } - } + httpEndpoints = generateEndpointsFromDir(__dirname + "/endpoints") } \ No newline at end of file diff --git a/packages/server/src/controllers/ConfigController/index.js b/packages/server/src/controllers/ConfigController/index.js deleted file mode 100755 index 6b0209dd..00000000 --- a/packages/server/src/controllers/ConfigController/index.js +++ /dev/null @@ -1,12 +0,0 @@ -import { Controller } from "linebridge/dist/server" - -export default class ConfigController extends Controller { - static refName = "ConfigController" - static useMiddlewares = ["withAuthentication", "onlyAdmin"] - - post = { - "/update_config": async (req, res) => { - - }, - } -} \ No newline at end of file diff --git a/packages/server/src/controllers/FeaturedEventsController/index.js b/packages/server/src/controllers/FeaturedEventsController/index.js index f7281706..80ee2642 100755 --- a/packages/server/src/controllers/FeaturedEventsController/index.js +++ b/packages/server/src/controllers/FeaturedEventsController/index.js @@ -1,61 +1,63 @@ import { Controller } from "linebridge/dist/server" -import { FeaturedEvent } from "../../models" -import createFeaturedEvent from "./methods/createFeaturedEvent" +import { FeaturedEvent } from "@models" +import createFeaturedEvent from "./services/createFeaturedEvent" + +// TODO: Migrate to new linebridge 0.15 endpoint classes instead of this export default class FeaturedEventsController extends Controller { - get = { - "/featured_event/:id": async (req, res) => { - const { id } = req.params - - const featuredEvent = await FeaturedEvent.findById(id) - - return res.json(featuredEvent) - }, - "/featured_events": async (req, res) => { - let query = { - expired: false - } - - if (req.query.includeExpired) { - delete query.expired - } - - const featuredEvents = await FeaturedEvent.find(query) - - return res.json(featuredEvents) - } - } - - post = { - "/featured_event": { - middlewares: ["withAuthentication", "onlyAdmin"], - fn: async (req, res) => { - const result = await createFeaturedEvent(req.body).catch((err) => { - res.status(500).json({ - error: err.message - }) - - return null - }) - - if (result) { - return res.json(result) - } - } - } - } - - delete = { - "/featured_event/:id": { - middlewares: ["withAuthentication", "onlyAdmin"], - fn: async (req, res) => { + httpEndpoints = { + get: { + "/featured_event/:id": async (req, res) => { const { id } = req.params - const featuredEvent = await FeaturedEvent.findByIdAndDelete(id) + const featuredEvent = await FeaturedEvent.findById(id) return res.json(featuredEvent) + }, + "/featured_events": async (req, res) => { + let query = { + expired: false + } + + if (req.query.includeExpired) { + delete query.expired + } + + const featuredEvents = await FeaturedEvent.find(query) + + return res.json(featuredEvents) } - } + }, + post: { + "/featured_event": { + middlewares: ["withAuthentication", "onlyAdmin"], + fn: async (req, res) => { + const result = await createFeaturedEvent(req.body).catch((err) => { + res.status(500).json({ + error: err.message + }) + + return null + }) + + if (result) { + return res.json(result) + } + } + } + }, + delete: { + "/featured_event/:id": { + middlewares: ["withAuthentication", "onlyAdmin"], + fn: async (req, res) => { + const { id } = req.params + + const featuredEvent = await FeaturedEvent.findByIdAndDelete(id) + + return res.json(featuredEvent) + } + } + }, } } \ No newline at end of file diff --git a/packages/server/src/controllers/FeaturedEventsController/methods/createFeaturedEvent.js b/packages/server/src/controllers/FeaturedEventsController/services/createFeaturedEvent.js similarity index 100% rename from packages/server/src/controllers/FeaturedEventsController/methods/createFeaturedEvent.js rename to packages/server/src/controllers/FeaturedEventsController/services/createFeaturedEvent.js diff --git a/packages/server/src/controllers/FeedController/index.js b/packages/server/src/controllers/FeedController/index.js index 5f23b96d..e4b0e2c8 100755 --- a/packages/server/src/controllers/FeedController/index.js +++ b/packages/server/src/controllers/FeedController/index.js @@ -1,61 +1,63 @@ import { Controller } from "linebridge/dist/server" -import getPosts from "./methods/getPosts" -import getPlaylists from "./methods/getPlaylists" +import getPosts from "./services/getPosts" +import getPlaylists from "./services/getPlaylists" export default class FeedController extends Controller { static refName = "FeedController" static useRoute = "/feed" - get = { - "/posts": { - middlewares: ["withAuthentication"], - fn: async (req, res) => { - const for_user_id = req.user?._id.toString() + httpEndpoints = { + get: { + "/posts": { + middlewares: ["withAuthentication"], + fn: async (req, res) => { + const for_user_id = req.user?._id.toString() - if (!for_user_id) { - return res.status(400).json({ - error: "Invalid user id" + if (!for_user_id) { + return res.status(400).json({ + error: "Invalid user id" + }) + } + + let feed = [] + + // fetch posts + const posts = await getPosts({ + for_user_id, + limit: req.query?.limit, + skip: req.query?.trim, }) + + feed = feed.concat(posts) + + return res.json(feed) } + }, + "/playlists": { + middlewares: ["withAuthentication"], + fn: async (req, res) => { + const for_user_id = req.user?._id.toString() - let feed = [] + if (!for_user_id) { + return res.status(400).json({ + error: "Invalid user id" + }) + } - // fetch posts - const posts = await getPosts({ - for_user_id, - limit: req.query?.limit, - skip: req.query?.trim, - }) + let feed = [] - feed = feed.concat(posts) - - return res.json(feed) - } - }, - "/playlists": { - middlewares: ["withAuthentication"], - fn: async (req, res) => { - const for_user_id = req.user?._id.toString() - - if (!for_user_id) { - return res.status(400).json({ - error: "Invalid user id" + // fetch playlists + const playlists = await getPlaylists({ + for_user_id, + limit: req.query?.limit, + skip: req.query?.trim, }) + + feed = feed.concat(playlists) + + return res.json(feed) } - - let feed = [] - - // fetch playlists - const playlists = await getPlaylists({ - for_user_id, - limit: req.query?.limit, - skip: req.query?.trim, - }) - - feed = feed.concat(playlists) - - return res.json(feed) } } } diff --git a/packages/server/src/controllers/FeedController/methods/getPlaylists.js b/packages/server/src/controllers/FeedController/services/getPlaylists.js similarity index 100% rename from packages/server/src/controllers/FeedController/methods/getPlaylists.js rename to packages/server/src/controllers/FeedController/services/getPlaylists.js diff --git a/packages/server/src/controllers/FeedController/methods/getPosts.js b/packages/server/src/controllers/FeedController/services/getPosts.js similarity index 100% rename from packages/server/src/controllers/FeedController/methods/getPosts.js rename to packages/server/src/controllers/FeedController/services/getPosts.js diff --git a/packages/server/src/controllers/FilesController/index.js b/packages/server/src/controllers/FilesController/index.js index f00ab40a..3659d972 100755 --- a/packages/server/src/controllers/FilesController/index.js +++ b/packages/server/src/controllers/FilesController/index.js @@ -1,24 +1,28 @@ import { Controller } from "linebridge/dist/server" -import uploadBodyFiles from "./methods/uploadBodyFiles" +import uploadBodyFiles from "./services/uploadBodyFiles" export default class FilesController extends Controller { - post = { - "/upload": { - middlewares: ["withAuthentication"], - fn: async (req, res) => { - const results = await uploadBodyFiles({ - req, - }).catch((err) => { - res.status(400).json({ - error: err.message, + static refName = "FilesController" + + httpEndpoints = { + post: { + "/upload": { + middlewares: ["withAuthentication"], + fn: async (req, res) => { + const results = await uploadBodyFiles({ + req, + }).catch((err) => { + res.status(400).json({ + error: err.message, + }) + + return false }) - return false - }) - - if (results) { - return res.json(results) + if (results) { + return res.json(results) + } } } } diff --git a/packages/server/src/controllers/FilesController/methods/uploadBodyFiles.js b/packages/server/src/controllers/FilesController/services/uploadBodyFiles.js similarity index 100% rename from packages/server/src/controllers/FilesController/methods/uploadBodyFiles.js rename to packages/server/src/controllers/FilesController/services/uploadBodyFiles.js diff --git a/packages/server/src/controllers/FollowController/endpoints/getFollowStatus.js b/packages/server/src/controllers/FollowController/endpoints/getFollowStatus.js new file mode 100644 index 00000000..65738433 --- /dev/null +++ b/packages/server/src/controllers/FollowController/endpoints/getFollowStatus.js @@ -0,0 +1,17 @@ +import { UserFollow } from "@models" + +export default { + method: "GET", + route: "/user/:user_id/is_followed", + middlewares: ["withAuthentication"], + fn: async (req, res) => { + const isFollowed = await UserFollow.findOne({ + user_id: req.user._id.toString(), + to: req.params.user_id, + }).catch(() => false) + + return res.json({ + isFollowed: Boolean(isFollowed), + }) + } +} \ No newline at end of file diff --git a/packages/server/src/controllers/FollowController/endpoints/getUserFollowers.js b/packages/server/src/controllers/FollowController/endpoints/getUserFollowers.js new file mode 100644 index 00000000..0fe7e972 --- /dev/null +++ b/packages/server/src/controllers/FollowController/endpoints/getUserFollowers.js @@ -0,0 +1,29 @@ +import { User, UserFollow } from "@lib" + +export default { + method: "GET", + route: "/user/:user_id/followers", + fn: async (req, res) => { + const { limit = 30, offset } = req.query + + let followers = [] + + const follows = await UserFollow.find({ + to: req.params.user_id, + }) + .limit(limit) + .skip(offset) + + for await (const follow of follows) { + const user = await User.findById(follow.user_id) + + if (!user) { + continue + } + + followers.push(user.toObject()) + } + + return res.json(followers) + } +} \ No newline at end of file diff --git a/packages/server/src/controllers/FollowController/endpoints/toogleFollow.js b/packages/server/src/controllers/FollowController/endpoints/toogleFollow.js new file mode 100644 index 00000000..e350c28d --- /dev/null +++ b/packages/server/src/controllers/FollowController/endpoints/toogleFollow.js @@ -0,0 +1,59 @@ +import { Schematized } from "@lib" +import { User, UserFollow } from "@models" + +import followUser from "../services/followUser" +import unfollowUser from "../services/unfollowUser" + +export default { + method: "POST", + route: "/user/toogle", + middlewares: ["withAuthentication"], + fn: Schematized({ + select: ["user_id", "username"], + }, async (req, res) => { + const selfUserId = req.user._id.toString() + let targetUserId = null + let result = null + + if (typeof req.selection.user_id === "undefined" && typeof req.selection.username === "undefined") { + return res.status(400).json({ message: "No user_id or username provided" }) + } + + if (typeof req.selection.user_id !== "undefined") { + targetUserId = req.selection.user_id + } else { + const user = await User.findOne({ username: req.selection.username }) + + if (!user) { + return res.status(404).json({ message: "User not found" }) + } + + targetUserId = user._id.toString() + } + + // check if already following + const isFollowed = await UserFollow.findOne({ + user_id: selfUserId, + to: targetUserId, + }) + + // if already following, delete + if (isFollowed) { + result = await unfollowUser({ + user_id: selfUserId, + to: targetUserId, + }).catch((error) => { + return res.status(500).json({ message: error.message }) + }) + } else { + result = await followUser({ + user_id: selfUserId, + to: targetUserId, + }).catch((error) => { + return res.status(500).json({ message: error.message }) + }) + } + + return res.json(result) + }) +} \ No newline at end of file diff --git a/packages/server/src/controllers/FollowController/index.js b/packages/server/src/controllers/FollowController/index.js new file mode 100755 index 00000000..abca8d13 --- /dev/null +++ b/packages/server/src/controllers/FollowController/index.js @@ -0,0 +1,9 @@ +import { Controller } from "linebridge/dist/server" +import generateEndpointsFromDir from "linebridge/dist/server/lib/generateEndpointsFromDir" + +export default class FollowController extends Controller { + static refName = "FollowController" + static useRoute = "/follow" + + httpEndpoints = generateEndpointsFromDir(__dirname + "/endpoints") +} \ No newline at end of file diff --git a/packages/server/src/controllers/FollowController/services/followUser.js b/packages/server/src/controllers/FollowController/services/followUser.js new file mode 100644 index 00000000..985106a3 --- /dev/null +++ b/packages/server/src/controllers/FollowController/services/followUser.js @@ -0,0 +1,48 @@ +import { User, UserFollow } from "@models" + +export default async (payload) => { + if (typeof payload.user_id === "undefined") { + throw new Error("No user_id provided") + } + if (typeof payload.to === "undefined") { + throw new Error("No to provided") + } + + const user = await User.findById(payload.user_id) + + if (!user) { + throw new Error("User not found") + } + + const follow = await UserFollow.findOne({ + user_id: payload.user_id, + to: payload.to, + }) + + if (follow) { + throw new Error("Already following") + } + + const newFollow = await UserFollow.create({ + user_id: payload.user_id, + to: payload.to, + }) + + await newFollow.save() + + global.wsInterface.io.emit(`user.follow`, { + ...user.toObject(), + }) + global.wsInterface.io.emit(`user.follow.${payload.user_id}`, { + ...user.toObject(), + }) + + const followers = await UserFollow.find({ + to: payload.to, + }) + + return { + following: true, + followers: followers, + } +} \ No newline at end of file diff --git a/packages/server/src/controllers/FollowController/services/unfollowUser.js b/packages/server/src/controllers/FollowController/services/unfollowUser.js new file mode 100644 index 00000000..9736d674 --- /dev/null +++ b/packages/server/src/controllers/FollowController/services/unfollowUser.js @@ -0,0 +1,43 @@ +import { User, UserFollow } from "@models" + +export default async (payload) => { + if (typeof payload.user_id === "undefined") { + throw new Error("No user_id provided") + } + if (typeof payload.to === "undefined") { + throw new Error("No to provided") + } + + const user = await User.findById(payload.user_id) + + if (!user) { + throw new Error("User not found") + } + + const follow = await UserFollow.findOne({ + user_id: payload.user_id, + to: payload.to, + }) + + if (!follow) { + throw new Error("Not following") + } + + await follow.remove() + + global.wsInterface.io.emit(`user.unfollow`, { + ...user.toObject(), + }) + global.wsInterface.io.emit(`user.unfollow.${payload.user_id}`, { + ...user.toObject(), + }) + + const followers = await UserFollow.find({ + to: payload.to, + }) + + return { + following: false, + followers: followers, + } +} \ No newline at end of file diff --git a/packages/server/src/controllers/FollowerController/index.js b/packages/server/src/controllers/FollowerController/index.js deleted file mode 100755 index 34805c41..00000000 --- a/packages/server/src/controllers/FollowerController/index.js +++ /dev/null @@ -1,213 +0,0 @@ -import { Controller } from "linebridge/dist/server" - -import { User, UserFollow } from "../../models" -import { Schematized } from "../../lib" - -export default class FollowerController extends Controller { - methods = { - follow: async (payload) => { - if (typeof payload.user_id === "undefined") { - throw new Error("No user_id provided") - } - if (typeof payload.to === "undefined") { - throw new Error("No to provided") - } - - const user = await User.findById(payload.user_id) - - if (!user) { - throw new Error("User not found") - } - - const follow = await UserFollow.findOne({ - user_id: payload.user_id, - to: payload.to, - }) - - if (follow) { - throw new Error("Already following") - } - - const newFollow = await UserFollow.create({ - user_id: payload.user_id, - to: payload.to, - }) - - await newFollow.save() - - global.wsInterface.io.emit(`user.follow`, { - ...user.toObject(), - }) - global.wsInterface.io.emit(`user.follow.${payload.user_id}`, { - ...user.toObject(), - }) - - const followers = await UserFollow.find({ - to: payload.to, - }) - - return { - following: true, - followers: followers, - } - }, - unfollow: async (payload) => { - if (typeof payload.user_id === "undefined") { - throw new Error("No user_id provided") - } - if (typeof payload.to === "undefined") { - throw new Error("No to provided") - } - - const user = await User.findById(payload.user_id) - - if (!user) { - throw new Error("User not found") - } - - const follow = await UserFollow.findOne({ - user_id: payload.user_id, - to: payload.to, - }) - - if (!follow) { - throw new Error("Not following") - } - - await follow.remove() - - global.wsInterface.io.emit(`user.unfollow`, { - ...user.toObject(), - }) - global.wsInterface.io.emit(`user.unfollow.${payload.user_id}`, { - ...user.toObject(), - }) - - const followers = await UserFollow.find({ - to: payload.to, - }) - - return { - following: false, - followers: followers, - } - }, - } - - post = { - "/follow_user": { - middlewares: ["withAuthentication"], - fn: Schematized({ - select: ["user_id", "username"], - }, async (req, res) => { - const selfUserId = req.user._id.toString() - let targetUserId = null - let result = null - - if (typeof req.selection.user_id === "undefined" && typeof req.selection.username === "undefined") { - return res.status(400).json({ message: "No user_id or username provided" }) - } - - if (typeof req.selection.user_id !== "undefined") { - targetUserId = req.selection.user_id - } else { - const user = await User.findOne({ username: req.selection.username }) - - if (!user) { - return res.status(404).json({ message: "User not found" }) - } - - targetUserId = user._id.toString() - } - - // check if already following - const isFollowed = await UserFollow.findOne({ - user_id: selfUserId, - to: targetUserId, - }) - - // if already following, delete - if (isFollowed) { - result = await this.methods.unfollow({ - user_id: selfUserId, - to: targetUserId, - }).catch((error) => { - return res.status(500).json({ message: error.message }) - }) - } else { - result = await this.methods.follow({ - user_id: selfUserId, - to: targetUserId, - }).catch((error) => { - return res.status(500).json({ message: error.message }) - }) - } - - return res.json(result) - }) - } - } - - get = { - "/user/:user_id/followers": async (req, res) => { - const { limit = 30, offset } = req.query - - let followers = [] - - const follows = await UserFollow.find({ - to: req.params.user_id, - }) - .limit(limit) - .skip(offset) - - for await (const follow of follows) { - const user = await User.findById(follow.user_id) - - if (!user) { - continue - } - - followers.push(user.toObject()) - } - - return res.json(followers) - }, - "/followers": Schematized({ - required: ["user_id"], - select: ["user_id"], - }, async (req, res) => { - let followers = [] - const follows = await UserFollow.find({ - to: req.selection.user_id, - }) - - for await (const follow of follows) { - const user = await User.findById(follow.user_id) - - if (!user) { - continue - } - - followers.push(user.toObject()) - } - - return res.json(followers) - }), - "/is_followed": { - middlewares: ["withAuthentication"], - fn: Schematized({ - required: ["user_id"], - select: ["user_id"] - }, async (req, res) => { - const isFollowed = await UserFollow.findOne({ - user_id: req.user._id.toString(), - to: req.selection.user_id, - }).catch(() => false) - - return res.json({ - isFollowed: Boolean(isFollowed), - }) - }), - }, - } -} \ No newline at end of file diff --git a/packages/server/src/controllers/PlaylistsController/index.js b/packages/server/src/controllers/PlaylistsController/index.js index ee6e75f2..ee741fc4 100755 --- a/packages/server/src/controllers/PlaylistsController/index.js +++ b/packages/server/src/controllers/PlaylistsController/index.js @@ -1,49 +1,18 @@ import { Controller } from "linebridge/dist/server" -import { Schematized } from "../../lib" +import { Schematized } from "@lib" -import publishPlaylist from "./methods/publishPlaylist" -import getPlaylist from "./methods/getPlaylist" +import publishPlaylist from "./services/publishPlaylist" +import getPlaylist from "./services/getPlaylist" export default class PlaylistsController extends Controller { - //static useMiddlewares = ["withAuthentication"] + static refName = "PlaylistsController" + static useRoute = "/playlist" - get = { - "/playlist/:id": async (req, res) => { - const result = await getPlaylist({ - _id: req.params.id - }).catch((err) => { - res.status(500).json({ - error: err.message - }) - - return null - }) - - if (result) { - return res.json(result) - } - } - } - - post = { - "/playlist/publish": { - middlewares: ["withAuthentication"], - fn: Schematized({ - required: ["title", "list"], - select: ["title", "description", "thumbnail", "list"], - }, async (req, res) => { - if (typeof req.body.list === "undefined") { - return res.status(400).json({ - error: "list is required" - }) - } - - // parse - req.selection.list = JSON.parse(req.selection.list) - - const result = await publishPlaylist({ - user_id: req.user._id.toString(), - ...req.selection + httpEndpoints = { + get: { + "/:id": async (req, res) => { + const result = await getPlaylist({ + _id: req.params.id }).catch((err) => { res.status(500).json({ error: err.message @@ -55,7 +24,41 @@ export default class PlaylistsController extends Controller { if (result) { return res.json(result) } - }) + } + }, + + post: { + "/publish": { + middlewares: ["withAuthentication"], + fn: Schematized({ + required: ["title", "list"], + select: ["title", "description", "thumbnail", "list"], + }, async (req, res) => { + if (typeof req.body.list === "undefined") { + return res.status(400).json({ + error: "list is required" + }) + } + + // parse + req.selection.list = JSON.parse(req.selection.list) + + const result = await publishPlaylist({ + user_id: req.user._id.toString(), + ...req.selection + }).catch((err) => { + res.status(500).json({ + error: err.message + }) + + return null + }) + + if (result) { + return res.json(result) + } + }) + } } } } \ No newline at end of file diff --git a/packages/server/src/controllers/PlaylistsController/methods/getPlaylist.js b/packages/server/src/controllers/PlaylistsController/services/getPlaylist.js similarity index 100% rename from packages/server/src/controllers/PlaylistsController/methods/getPlaylist.js rename to packages/server/src/controllers/PlaylistsController/services/getPlaylist.js diff --git a/packages/server/src/controllers/PlaylistsController/methods/publishPlaylist.js b/packages/server/src/controllers/PlaylistsController/services/publishPlaylist.js similarity index 100% rename from packages/server/src/controllers/PlaylistsController/methods/publishPlaylist.js rename to packages/server/src/controllers/PlaylistsController/services/publishPlaylist.js diff --git a/packages/server/src/controllers/PostsController/endpoints/createPost.js b/packages/server/src/controllers/PostsController/endpoints/createPost.js new file mode 100644 index 00000000..4686cc18 --- /dev/null +++ b/packages/server/src/controllers/PostsController/endpoints/createPost.js @@ -0,0 +1,23 @@ +import { Schematized } from "@lib" +import { CreatePost } from "../methods" + +export default { + method: "POST", + route: "/new", + middlewares: ["withAuthentication"], + fn: Schematized({ + required: ["timestamp"], + select: ["message", "attachments", "type", "data", "timestamp"], + }, async (req, res) => { + const post = await CreatePost({ + user_id: req.user.id, + message: req.selection.message, + timestamp: req.selection.timestamp, + attachments: req.selection.attachments, + type: req.selection.type, + data: req.selection.data, + }) + + return res.json(post) + }) +} \ No newline at end of file diff --git a/packages/server/src/controllers/PostsController/endpoints/deletePost.js b/packages/server/src/controllers/PostsController/endpoints/deletePost.js new file mode 100644 index 00000000..39015fec --- /dev/null +++ b/packages/server/src/controllers/PostsController/endpoints/deletePost.js @@ -0,0 +1,26 @@ +import { DeletePost } from "../methods" + +export default { + method: "DELETE", + route: "/:post_id", + middlewares: ["withAuthentication"], + fn: async (req, res) => { + const post = await DeletePost({ + post_id: req.params.post_id, + by_user_id: req.user._id.toString(), + }).catch((err) => { + res.status(400).json({ + error: err.message + }) + + return false + }) + + if (!post) return + + return res.json({ + success: true, + post + }) + } +} \ No newline at end of file diff --git a/packages/server/src/controllers/PostsController/endpoints/explorePosts.js b/packages/server/src/controllers/PostsController/endpoints/explorePosts.js new file mode 100644 index 00000000..f4ff9e35 --- /dev/null +++ b/packages/server/src/controllers/PostsController/endpoints/explorePosts.js @@ -0,0 +1,20 @@ +import { Schematized } from "@lib" +import { GetPostData } from "../methods" + +export default { + method: "GET", + route: "/explore", + middlewares: ["withOptionalAuthentication"], + fn: Schematized({ + select: ["user_id"] + }, async (req, res) => { + let posts = await GetPostData({ + limit: req.query?.limit, + skip: req.query?.trim, + from_user_id: req.query?.user_id, + for_user_id: req.user?._id.toString(), + }) + + return res.json(posts) + }) +} \ No newline at end of file diff --git a/packages/server/src/controllers/PostsController/endpoints/getPostData.js b/packages/server/src/controllers/PostsController/endpoints/getPostData.js new file mode 100644 index 00000000..1b24803a --- /dev/null +++ b/packages/server/src/controllers/PostsController/endpoints/getPostData.js @@ -0,0 +1,21 @@ +import { GetPostData } from "../methods" + +export default { + method: "GET", + route: "/:post_id", + middlewares: ["withOptionalAuthentication"], + fn: async (req, res) => { + let post = await GetPostData({ + post_id: req.params.post_id, + for_user_id: req.user?._id.toString(), + }).catch((error) => { + res.status(404).json({ error: error.message }) + + return null + }) + + if (!post) return + + return res.json(post) + } +} \ No newline at end of file diff --git a/packages/server/src/controllers/PostsController/endpoints/getPostFromUser.js b/packages/server/src/controllers/PostsController/endpoints/getPostFromUser.js new file mode 100644 index 00000000..96bec9ae --- /dev/null +++ b/packages/server/src/controllers/PostsController/endpoints/getPostFromUser.js @@ -0,0 +1,17 @@ +import { GetPostData } from "../methods" + +export default { + method: "GET", + route: "/user/:user_id", + middlewares: ["withOptionalAuthentication"], + fn: async (req, res) => { + let posts = await GetPostData({ + limit: req.query?.limit, + skip: req.query?.trim, + for_user_id: req.user?._id.toString(), + from_user_id: req.params.user_id, + }) + + return res.json(posts) + } +} \ No newline at end of file diff --git a/packages/server/src/controllers/PostsController/endpoints/savedPosts.js b/packages/server/src/controllers/PostsController/endpoints/savedPosts.js new file mode 100644 index 00000000..e9f9f29d --- /dev/null +++ b/packages/server/src/controllers/PostsController/endpoints/savedPosts.js @@ -0,0 +1,20 @@ +import { Schematized } from "@lib" +import { GetPostData } from "../methods" + +export default { + method: "GET", + route: "/saved", + middlewares: ["withAuthentication"], + fn: Schematized({ + select: ["user_id"] + }, async (req, res) => { + let posts = await GetPostData({ + limit: req.query?.limit, + skip: req.query?.trim, + for_user_id: req.user?._id.toString(), + savedOnly: true, + }) + + return res.json(posts) + }) +} \ No newline at end of file diff --git a/packages/server/src/controllers/PostsController/endpoints/toogleLike.js b/packages/server/src/controllers/PostsController/endpoints/toogleLike.js new file mode 100644 index 00000000..9b50b035 --- /dev/null +++ b/packages/server/src/controllers/PostsController/endpoints/toogleLike.js @@ -0,0 +1,29 @@ +import { Schematized } from "@lib" +import { ToogleLike } from "../methods" + +export default { + method: "GET", + route: "/:post_id/toogle_like", + middlewares: ["withAuthentication"], + fn: Schematized({ + select: ["to"], + }, async (req, res) => { + const post = await ToogleLike({ + user_id: req.user._id.toString(), + post_id: req.params.post_id, + to: req.selection.to, + }).catch((err) => { + res.status(400).json({ + error: err.message + }) + return false + }) + + if (!post) return + + return res.json({ + success: true, + post + }) + }) +} \ No newline at end of file diff --git a/packages/server/src/controllers/PostsController/endpoints/toogleSave.js b/packages/server/src/controllers/PostsController/endpoints/toogleSave.js new file mode 100644 index 00000000..25b180db --- /dev/null +++ b/packages/server/src/controllers/PostsController/endpoints/toogleSave.js @@ -0,0 +1,25 @@ +import { ToogleSavePost } from "../methods" + +export default { + method: "POST", + route: "/:post_id/toogle_save", + middlewares: ["withAuthentication"], + fn: async (req, res) => { + const post = await ToogleSavePost({ + user_id: req.user._id.toString(), + post_id: req.params.post_id, + }).catch((err) => { + res.status(400).json({ + error: err.message + }) + return false + }) + + if (!post) return + + return res.json({ + success: true, + post + }) + } +} \ No newline at end of file diff --git a/packages/server/src/controllers/PostsController/index.js b/packages/server/src/controllers/PostsController/index.js index 9c63cbdd..6bed9f4a 100755 --- a/packages/server/src/controllers/PostsController/index.js +++ b/packages/server/src/controllers/PostsController/index.js @@ -1,173 +1,19 @@ import { Controller } from "linebridge/dist/server" -import { Schematized } from "../../lib" - -import { CreatePost, ToogleLike, GetPostData, DeletePost, ToogleSavePost } from "./methods" +import generateEndpointsFromDir from "linebridge/dist/server/lib/generateEndpointsFromDir" export default class PostsController extends Controller { static refName = "PostsController" static useRoute = "/posts" - get = { - "/explore": { - middlewares: ["withOptionalAuthentication"], - fn: Schematized({ - select: ["user_id"] - }, async (req, res) => { - let posts = await GetPostData({ - limit: req.query?.limit, - skip: req.query?.trim, - from_user_id: req.query?.user_id, - for_user_id: req.user?._id.toString(), - }) + httpEndpoints = generateEndpointsFromDir(__dirname + "/endpoints") - return res.json(posts) - }) - }, - "/saved": { - middlewares: ["withOptionalAuthentication"], - fn: Schematized({ - select: ["user_id"] - }, async (req, res) => { - let posts = await GetPostData({ - limit: req.query?.limit, - skip: req.query?.trim, - for_user_id: req.user?._id.toString(), - savedOnly: true, - }) - - return res.json(posts) - }) - }, - "/user/:user_id": { - middlewares: ["withOptionalAuthentication"], - fn: async (req, res) => { - let posts = await GetPostData({ - limit: req.query?.limit, - skip: req.query?.trim, - for_user_id: req.user?._id.toString(), - from_user_id: req.params.user_id, - }) - - return res.json(posts) - } - }, - "/:post_id": { - middlewares: ["withOptionalAuthentication"], - fn: async (req, res) => { - let post = await GetPostData({ - post_id: req.params.post_id, - for_user_id: req.user?._id.toString(), - }).catch((error) => { - res.status(404).json({ error: error.message }) - - return null - }) - - if (!post) return - - return res.json(post) - } - }, - } - - put = { - "/:post_id": { - middlewares: ["withAuthentication"], - fn: (req, res) => { - // TODO: Implement Post update - return res.status(501).json({ error: "Not implemented" }) - } - } - } - - post = { - "/new": { - middlewares: ["withAuthentication"], - fn: Schematized({ - required: ["timestamp"], - select: ["message", "attachments", "type", "data", "timestamp"], - }, async (req, res) => { - const post = await CreatePost({ - user_id: req.user.id, - message: req.selection.message, - timestamp: req.selection.timestamp, - attachments: req.selection.attachments, - type: req.selection.type, - data: req.selection.data, - }) - - return res.json(post) - }) - }, - "/:post_id/toogle_like": { - middlewares: ["withAuthentication"], - fn: Schematized({ - select: ["to"], - }, async (req, res) => { - const post = await ToogleLike({ - user_id: req.user._id.toString(), - post_id: req.params.post_id, - to: req.selection.to, - }).catch((err) => { - res.status(400).json({ - error: err.message - }) - return false - }) - - if (!post) return - - return res.json({ - success: true, - post - }) - }) - }, - "/:post_id/toogle_save": { - middlewares: ["withAuthentication"], - fn: async (req, res) => { - const post = await ToogleSavePost({ - user_id: req.user._id.toString(), - post_id: req.params.post_id, - }).catch((err) => { - res.status(400).json({ - error: err.message - }) - return false - }) - - if (!post) return - - return res.json({ - success: true, - post - }) - } - } - } - - delete = { - "/:post_id": { - middlewares: ["withAuthentication"], - fn: async (req, res) => { - const post = await DeletePost({ - post_id: req.params.post_id, - by_user_id: req.user._id.toString(), - }).catch((err) => { - res.status(400).json({ - error: err.message - }) - - return false - }) - - if (!post) return - - return res.json({ - success: true, - post - }) - } - }, - } + // put = { + // "/:post_id": { + // middlewares: ["withAuthentication"], + // fn: (req, res) => { + // // TODO: Implement Post update + // return res.status(501).json({ error: "Not implemented" }) + // } + // } + // } } \ No newline at end of file diff --git a/packages/server/src/controllers/PublicController/endpoints/featuredWallpapers.js b/packages/server/src/controllers/PublicController/endpoints/featuredWallpapers.js new file mode 100644 index 00000000..1f712f0c --- /dev/null +++ b/packages/server/src/controllers/PublicController/endpoints/featuredWallpapers.js @@ -0,0 +1,18 @@ +import { FeaturedWallpaper } from "../../../models" + +export default { + method: "GET", + route: "/featured_wallpapers", + fn: async (req, res) => { + const featuredWallpapers = await FeaturedWallpaper.find({}) + .sort({ date: -1 }) + .limit(10) + .catch(err => { + return res.status(500).json({ + error: err.message + }).end() + }) + + return res.json(featuredWallpapers) + } +} \ No newline at end of file diff --git a/packages/server/src/controllers/PublicController/endpoints/incidentPrediction.js b/packages/server/src/controllers/PublicController/endpoints/incidentPrediction.js new file mode 100644 index 00000000..2e5e1917 --- /dev/null +++ b/packages/server/src/controllers/PublicController/endpoints/incidentPrediction.js @@ -0,0 +1,27 @@ +import { Schematized } from "../../../lib" +import IndecentPrediction from "../../../utils/indecent-prediction" + +export default { + method: "GET", + route: "/indecent_prediction", + fn: Schematized({ + select: ["url"], + required: ["url"], + }, async (req, res) => { + const { url } = req.selection + + const predictions = await IndecentPrediction({ + url, + }).catch((err) => { + res.status(500).json({ + error: err.message, + }) + + return null + }) + + if (predictions) { + return res.json(predictions) + } + }) +} \ No newline at end of file diff --git a/packages/server/src/controllers/PublicController/endpoints/newFeaturedWallpaper.js b/packages/server/src/controllers/PublicController/endpoints/newFeaturedWallpaper.js new file mode 100644 index 00000000..23da2139 --- /dev/null +++ b/packages/server/src/controllers/PublicController/endpoints/newFeaturedWallpaper.js @@ -0,0 +1,23 @@ +import { Schematized } from "@lib" +import { FeaturedWallpaper } from "@models" + +export default { + method: "POST", + route: "/featured_wallpaper", + middlewares: ["withAuthentication", "onlyAdmin"], + fn: Schematized({ + select: ["url", "date", "author"], + required: ["url"], + }, async (req, res) => { + const newFeaturedWallpaper = new FeaturedWallpaper(req.selection) + + const result = await newFeaturedWallpaper.save().catch((err) => { + res.status(400).json({ message: err.message }) + return null + }) + + if (result) { + return res.json(newFeaturedWallpaper) + } + }) +} \ No newline at end of file diff --git a/packages/server/src/controllers/PublicController/endpoints/postingPolicy.js b/packages/server/src/controllers/PublicController/endpoints/postingPolicy.js new file mode 100644 index 00000000..74169649 --- /dev/null +++ b/packages/server/src/controllers/PublicController/endpoints/postingPolicy.js @@ -0,0 +1,9 @@ +export default { + method: "GET", + route: "/posting_policy", + middlewares: ["withOptionalAuthentication"], + fn: async (req, res) => { + // TODO: Use `PermissionsAPI` to get the user's permissions and return the correct policy, by now it will return the default policy + return res.json(global.DEFAULT_POSTING_POLICY) + } +} \ No newline at end of file diff --git a/packages/server/src/controllers/PublicController/index.js b/packages/server/src/controllers/PublicController/index.js index fdfbefce..b40895a8 100755 --- a/packages/server/src/controllers/PublicController/index.js +++ b/packages/server/src/controllers/PublicController/index.js @@ -1,86 +1,8 @@ import { Controller } from "linebridge/dist/server" -import { Schematized } from "../../lib" - -import { FeaturedWallpaper } from "../../models" - -import IndecentPrediction from "../../utils/indecent-prediction" +import generateEndpointsFromDir from "linebridge/dist/server/lib/generateEndpointsFromDir" export default class PublicController extends Controller { static refName = "PublicController" - get = { - "/indecent_prediction": { - fn: Schematized({ - select: ["url"], - required: ["url"], - }, async (req, res) => { - const { url } = req.selection - - const predictions = await IndecentPrediction({ - url, - }).catch((err) => { - res.status(500).json({ - error: err.message, - }) - - return null - }) - - if (predictions) { - return res.json(predictions) - } - }) - }, - "/posting_policy": { - middlewares: ["withOptionalAuthentication"], - fn: async (req, res) => { - // TODO: Use `PermissionsAPI` to get the user's permissions and return the correct policy, by now it will return the default policy - return res.json(global.DEFAULT_POSTING_POLICY) - } - }, - "/featured_wallpapers": { - fn: async (req, res) => { - const featuredWallpapers = await FeaturedWallpaper.find({}) - .sort({ date: -1 }) - .limit(10) - .catch(err => { - return res.status(500).json({ - error: err.message - }).end() - }) - - return res.json(featuredWallpapers) - } - } - } - - 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 - }) - }, - }, - "/new_featured_wallpaper": { - middlewares: ["withAuthentication", "onlyAdmin"], - fn: Schematized({ - select: ["url", "date", "author"], - required: ["url"], - }, async (req, res) => { - const newFeaturedWallpaper = new FeaturedWallpaper(req.selection) - - const result = await newFeaturedWallpaper.save().catch((err) => { - res.status(400).json({ message: err.message }) - return null - }) - - if (result) { - return res.json(newFeaturedWallpaper) - } - }) - } - } + httpEndpoints = generateEndpointsFromDir(__dirname + "/endpoints") } \ No newline at end of file diff --git a/packages/server/src/controllers/RolesController/index.js b/packages/server/src/controllers/RolesController/index.js index 52a24c32..e0b7864f 100755 --- a/packages/server/src/controllers/RolesController/index.js +++ b/packages/server/src/controllers/RolesController/index.js @@ -1,113 +1,115 @@ import { Controller } from "linebridge/dist/server" -import { Role, User } from "../../models" -import { Schematized } from "../../lib" +import { Role, User } from "@models" +import { Schematized } from "@lib" export default class RolesController extends Controller { static refName = "RolesController" static useMiddlewares = ["roles"] - get = { - "/roles": Schematized({ - select: ["user_id", "username"], - }, async (req, res) => { - const roles = await Role.find() - - return res.json(roles) - }), - "/user_roles": { - middlewares: ["withAuthentication"], - fn: Schematized({ - select: ["username"], + httpEndpoints = { + get: { + "/roles": Schematized({ + select: ["user_id", "username"], }, async (req, res) => { - const user = await User.findOne(req.selection) + const roles = await Role.find() - if (!user) { - return res.status(404).json({ error: "No user founded" }) - } - - return res.json(user.roles) - }) - }, - } - - 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).json("You do not have administrator permission") - } - - if (!Array.isArray(req.selection.update)) { - return res.status(400).json("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.json("done") + return res.json(roles) }), - }, - } + "/user_roles": { + middlewares: ["withAuthentication"], + fn: Schematized({ + select: ["username"], + }, async (req, res) => { + const user = await User.findOne(req.selection) - 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") + if (!user) { + return res.status(404).json({ error: "No user founded" }) } - data.remove() - - return res.json(data) + return res.json(user.roles) }) - }) + }, + }, + + 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).json("You do not have administrator permission") + } + + if (!Array.isArray(req.selection.update)) { + return res.status(400).json("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.json("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) + }) + }) + }, }, } } \ No newline at end of file diff --git a/packages/server/src/controllers/SearchController/index.js b/packages/server/src/controllers/SearchController/index.js index 5ad46d42..b940512a 100755 --- a/packages/server/src/controllers/SearchController/index.js +++ b/packages/server/src/controllers/SearchController/index.js @@ -1,30 +1,36 @@ import { Controller } from "linebridge/dist/server" -import { User, Post } from "../../models" + +import { User, Post } from "@models" export default class SearchController extends Controller { - get = { - "/search": { - middlewares: ["withOptionalAuthentication"], - fn: async (req, res) => { - const { keywords = "" } = req.query + static refName = "SearchController" + static useRoute = "/search" - let suggestions = {} + httpEndpoints = { + get: { + "/": { + middlewares: ["withOptionalAuthentication"], + fn: async (req, res) => { + const { keywords = "" } = req.query - // search users by username or name - const users = await User.find({ - $or: [ - { username: { $regex: keywords, $options: "i" } }, - { fullName: { $regex: keywords, $options: "i" } }, - ], - }) - .limit(5) - .select("username fullName avatar verified") + let suggestions = {} - if (users.length > 0) { - suggestions["users"] = users + // search users by username or name + const users = await User.find({ + $or: [ + { username: { $regex: keywords, $options: "i" } }, + { fullName: { $regex: keywords, $options: "i" } }, + ], + }) + .limit(5) + .select("username fullName avatar verified") + + if (users.length > 0) { + suggestions["users"] = users + } + + return res.json(suggestions) } - - return res.json(suggestions) } } } diff --git a/packages/server/src/controllers/SessionController/endpoints/deleteAllSelfSessions.js b/packages/server/src/controllers/SessionController/endpoints/deleteAllSelfSessions.js new file mode 100644 index 00000000..002619e7 --- /dev/null +++ b/packages/server/src/controllers/SessionController/endpoints/deleteAllSelfSessions.js @@ -0,0 +1,16 @@ +export default { + method: "DELETE", + route: "/all", + middlewares: ["withAuthentication"], + fn: async (req, res) => { + const user_id = req.user._id.toString() + + const allSessions = await Session.deleteMany({ user_id }) + + if (allSessions) { + return res.json("done") + } + + return res.status(404).json("not found") + } +} \ No newline at end of file diff --git a/packages/server/src/controllers/SessionController/endpoints/deleteSelfSession.js b/packages/server/src/controllers/SessionController/endpoints/deleteSelfSession.js new file mode 100644 index 00000000..4801c330 --- /dev/null +++ b/packages/server/src/controllers/SessionController/endpoints/deleteSelfSession.js @@ -0,0 +1,27 @@ +import { Session } from "@models" + +export default { + method: "DELETE", + route: "/", + middlewares: ["withAuthentication"], + fn: async (req, res) => { + const { token } = req.body + const user_id = req.user._id.toString() + + if (typeof token === "undefined") { + return res.status(400).json("No token provided") + } + + const session = await Session.findOneAndDelete({ user_id, token }) + + if (session) { + return res.json({ + message: "done", + }) + } + + return res.status(404).json({ + error: "Session not found", + }) + }, +} \ No newline at end of file diff --git a/packages/server/src/controllers/SessionController/endpoints/getCurrentSession.js b/packages/server/src/controllers/SessionController/endpoints/getCurrentSession.js new file mode 100644 index 00000000..7a49da98 --- /dev/null +++ b/packages/server/src/controllers/SessionController/endpoints/getCurrentSession.js @@ -0,0 +1,8 @@ +export default { + method: "GET", + route: "/current", + middlewares: ["withAuthentication"], + fn: async (req, res) => { + return res.json(req.currentSession) + } +} \ No newline at end of file diff --git a/packages/server/src/controllers/SessionController/endpoints/getSessions.js b/packages/server/src/controllers/SessionController/endpoints/getSessions.js new file mode 100644 index 00000000..4ef774dd --- /dev/null +++ b/packages/server/src/controllers/SessionController/endpoints/getSessions.js @@ -0,0 +1,13 @@ +import { Session } from "@models" + +export default { + method: "GET", + route: "/all", + middlewares: ["withAuthentication"], + fn: async (req, res) => { + const sessions = await Session.find({ user_id: req.user._id.toString() }, { token: 0 }) + .sort({ date: -1 }) + + return res.json(sessions) + }, +} \ No newline at end of file diff --git a/packages/server/src/controllers/SessionController/endpoints/regenerateSessionToken.js b/packages/server/src/controllers/SessionController/endpoints/regenerateSessionToken.js new file mode 100644 index 00000000..0866b15d --- /dev/null +++ b/packages/server/src/controllers/SessionController/endpoints/regenerateSessionToken.js @@ -0,0 +1,20 @@ +import { Token } from "@lib" + +export default { + method: "POST", + route: "/regenerate", + middlewares: ["useJwtStrategy"], + fn: async (req, res) => { + const { expiredToken, refreshToken } = req.body + + const token = await Token.regenerateSession(expiredToken, refreshToken).catch((error) => { + res.status(400).json({ error: error.message }) + + return null + }) + + if (!token) return + + return res.json({ token }) + }, +} \ No newline at end of file diff --git a/packages/server/src/controllers/SessionController/endpoints/validateSession.js b/packages/server/src/controllers/SessionController/endpoints/validateSession.js new file mode 100644 index 00000000..54d8a26b --- /dev/null +++ b/packages/server/src/controllers/SessionController/endpoints/validateSession.js @@ -0,0 +1,47 @@ +import jwt from "jsonwebtoken" + +import { Session } from "@models" + +export default { + method: "POST", + route: "/validate", + middlewares: ["useJwtStrategy"], + fn: 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 + } + 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) + }, +} \ No newline at end of file diff --git a/packages/server/src/controllers/SessionController/index.js b/packages/server/src/controllers/SessionController/index.js index 37961f92..4455376d 100755 --- a/packages/server/src/controllers/SessionController/index.js +++ b/packages/server/src/controllers/SessionController/index.js @@ -1,130 +1,9 @@ import { Controller } from "linebridge/dist/server" -import jwt from "jsonwebtoken" - -import { Session } from "../../models" -import { Token } from "../../lib" +import generateEndpointsFromDir from "linebridge/dist/server/lib/generateEndpointsFromDir" export default class SessionController extends Controller { static refName = "SessionController" + static useRoute = "/session" - 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 }) - .sort({ date: -1 }) - - return res.json(sessions) - }, - }, - "/current_session": { - middlewares: ["withAuthentication"], - fn: async (req, res) => { - return res.json(req.currentSession) - } - }, - } - - post = { - "/validate_session": { - middlewares: ["useJwtStrategy"], - fn: 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 - } - 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) - }, - }, - "/regenerate_session_token": { - middlewares: ["useJwtStrategy"], - fn: async (req, res) => { - const { expiredToken, refreshToken } = req.body - - const token = await Token.regenerateSession(expiredToken, refreshToken).catch((error) => { - res.status(400).json({ error: error.message }) - - return null - }) - - if (!token) return - - return res.json({ token }) - }, - } - } - - delete = { - "/session": { - middlewares: ["withAuthentication"], - fn: async (req, res) => { - const { token, user_id } = req.body - - if (typeof user_id === "undefined") { - return res.status(400).json("No user_id provided") - } - if (typeof token === "undefined") { - return res.status(400).json("No token provided") - } - - const session = await Session.findOneAndDelete({ user_id, token }) - if (session) { - return res.json("done") - } - - return res.status(404).json("not found") - }, - }, - "/sessions": { - middlewares: ["withAuthentication"], - fn: async (req, res) => { - const { user_id } = req.body - - if (typeof user_id === "undefined") { - return res.status(400).json("No user_id provided") - } - - const allSessions = await Session.deleteMany({ user_id }) - if (allSessions) { - return res.json("done") - } - - return res.status(404).json("not found") - } - }, - } + httpEndpoints = generateEndpointsFromDir(__dirname + "/endpoints") } \ No newline at end of file diff --git a/packages/server/src/controllers/StreamingController/endpoints/getStreamAddresses.js b/packages/server/src/controllers/StreamingController/endpoints/getStreamAddresses.js new file mode 100644 index 00000000..55c8a670 --- /dev/null +++ b/packages/server/src/controllers/StreamingController/endpoints/getStreamAddresses.js @@ -0,0 +1,21 @@ +export default { + method: "GET", + route: "/stream/addresses", + middlewares: ["withOptionalAuthentication"], + fn: async (req, res) => { + const addresses = { + api: process.env.STREAMING_INGEST_SERVER, + ingest: process.env.STREAMING_API_SERVER, + } + + if (req.user) { + addresses.liveURL = `${addresses.api}/live/${req.user.username}` + addresses.ingestURL = `${addresses.ingest}/${req.user.username}` + + addresses.hlsURL = `${addresses.liveURL}/src.m3u8` + addresses.flvURL = `${addresses.liveURL}/src.flv` + } + + return res.json(addresses) + } +} \ No newline at end of file diff --git a/packages/server/src/controllers/StreamingController/endpoints/getStreamInfo.js b/packages/server/src/controllers/StreamingController/endpoints/getStreamInfo.js new file mode 100644 index 00000000..bb111be2 --- /dev/null +++ b/packages/server/src/controllers/StreamingController/endpoints/getStreamInfo.js @@ -0,0 +1,51 @@ +import { StreamingInfo, User, StreamingCategory } from "@models" + +export default { + method: "GET", + route: "/stream/info", + middleware: ["withAuthentication"], + fn: async (req, res) => { + let user_id = req.query.user_id + + if (!req.query.username && !req.query.user_id) { + return res.status(400).json({ + error: "Invalid request, missing username" + }) + } + + if (!user_id) { + user_id = await User.findOne({ + username: req.query.username, + }) + + user_id = user_id["_id"].toString() + } + + let info = await StreamingInfo.findOne({ + user_id, + }) + + if (!info) { + info = new StreamingInfo({ + user_id, + }) + + await info.save() + } + + const category = await StreamingCategory.findOne({ + key: info.category + }).catch((err) => { + console.error(err) + return {} + }) ?? {} + + return res.json({ + ...info.toObject(), + ["category"]: { + key: category?.key ?? "unknown", + label: category?.label ?? "Unknown", + } + }) + } +} \ No newline at end of file diff --git a/packages/server/src/controllers/StreamingController/endpoints/getStreamingCategories.js b/packages/server/src/controllers/StreamingController/endpoints/getStreamingCategories.js new file mode 100644 index 00000000..63d601f6 --- /dev/null +++ b/packages/server/src/controllers/StreamingController/endpoints/getStreamingCategories.js @@ -0,0 +1,11 @@ +import { StreamingCategory } from "@models" + +export default { + method: "GET", + route: "/streaming/categories", + fn: async (req, res) => { + const categories = await StreamingCategory.find() + + return res.json(categories) + } +} \ No newline at end of file diff --git a/packages/server/src/controllers/StreamingController/endpoints/getStreamingKey.js b/packages/server/src/controllers/StreamingController/endpoints/getStreamingKey.js new file mode 100644 index 00000000..8aaa8a63 --- /dev/null +++ b/packages/server/src/controllers/StreamingController/endpoints/getStreamingKey.js @@ -0,0 +1,31 @@ +import { StreamingKey } from "@models" +import generateStreamingKey from "../services/generateStreamingKey" + +export default { + method: "GET", + route: "/streaming/key", + middlewares: ["withAuthentication"], + fn: async (req, res) => { + let streamingKey = await StreamingKey.findOne({ + user_id: req.user._id.toString() + }) + + if (!streamingKey) { + const newKey = await generateStreamingKey(req.user._id.toString()).catch(err => { + res.status(500).json({ + error: `Cannot generate a new key: ${err.message}`, + }) + + return false + }) + + if (!newKey) { + return false + } + + return res.json(newKey) + } else { + return res.json(streamingKey) + } + } +} \ No newline at end of file diff --git a/packages/server/src/controllers/StreamingController/endpoints/getStreams.js b/packages/server/src/controllers/StreamingController/endpoints/getStreams.js new file mode 100644 index 00000000..cdd3d44c --- /dev/null +++ b/packages/server/src/controllers/StreamingController/endpoints/getStreams.js @@ -0,0 +1,11 @@ +import fetchStreamsFromAPI from "../services/fetchStreamsFromAPI" + +export default { + method: "GET", + route: "/streams", + fn: async (req, res) => { + const remoteStreams = await fetchStreamsFromAPI() + + return res.json(remoteStreams) + } +} \ No newline at end of file diff --git a/packages/server/src/controllers/StreamingController/endpoints/handleStreamPublish.js b/packages/server/src/controllers/StreamingController/endpoints/handleStreamPublish.js new file mode 100644 index 00000000..ccc4e075 --- /dev/null +++ b/packages/server/src/controllers/StreamingController/endpoints/handleStreamPublish.js @@ -0,0 +1,32 @@ +import generateStreamDataFromStreamingKey from "../services/generateStreamDataFromStreamingKey" + +// This endpoint is used by the streaming server to check if a stream is valid and to notify the clients that a stream has started + +export default { + method: "POST", + route: "/stream/publish", + fn: async (req, res) => { + const { stream } = req.body + + const streaming = await generateStreamDataFromStreamingKey(stream).catch((err) => { + console.error(err) + + res.status(500).json({ + error: `Cannot generate stream: ${err.message}`, + }) + + return null + }) + + if (streaming) { + global.wsInterface.io.emit(`streaming.new`, streaming) + + global.wsInterface.io.emit(`streaming.new.${streaming.username}`, streaming) + + return res.json({ + code: 0, + status: "ok" + }) + } + } +} \ No newline at end of file diff --git a/packages/server/src/controllers/StreamingController/endpoints/handleStreamUnpublish.js b/packages/server/src/controllers/StreamingController/endpoints/handleStreamUnpublish.js new file mode 100644 index 00000000..bcdfe830 --- /dev/null +++ b/packages/server/src/controllers/StreamingController/endpoints/handleStreamUnpublish.js @@ -0,0 +1,26 @@ +import generateStreamDataFromStreamingKey from "../services/generateStreamDataFromStreamingKey" + +export default { + method: "POST", + route: "/stream/unpublish", + fn: async (req, res) => { + const { stream } = req.body + + const streaming = await generateStreamDataFromStreamingKey(stream).catch((err) => { + console.error(err) + + return null + }) + + if (streaming) { + global.wsInterface.io.emit(`streaming.end`, streaming) + + global.wsInterface.io.emit(`streaming.end.${streaming.username}`, streaming) + + return res.json({ + code: 0, + status: "ok" + }) + } + } +} \ No newline at end of file diff --git a/packages/server/src/controllers/StreamingController/endpoints/regenerateStreamingKey.js b/packages/server/src/controllers/StreamingController/endpoints/regenerateStreamingKey.js new file mode 100644 index 00000000..6140f891 --- /dev/null +++ b/packages/server/src/controllers/StreamingController/endpoints/regenerateStreamingKey.js @@ -0,0 +1,35 @@ +import { StreamingKey } from "@models" +import generateStreamingKey from "../services/generateStreamingKey" + +export default { + method: "POST", + route: "/streaming/key/regenerate", + middlewares: ["withAuthentication"], + fn: async (req, res) => { + // check if the user already has a key + let streamingKey = await StreamingKey.findOne({ + user_id: req.user._id.toString() + }) + + // if exists, delete it + + if (streamingKey) { + await streamingKey.remove() + } + + // generate a new key + const newKey = await generateStreamingKey(req.user._id.toString()).catch(err => { + res.status(500).json({ + error: `Cannot generate a new key: ${err.message}`, + }) + + return false + }) + + if (!newKey) { + return false + } + + return res.json(newKey) + } +} \ No newline at end of file diff --git a/packages/server/src/controllers/StreamingController/endpoints/updateStreamInfo.js b/packages/server/src/controllers/StreamingController/endpoints/updateStreamInfo.js new file mode 100644 index 00000000..6ae5f6be --- /dev/null +++ b/packages/server/src/controllers/StreamingController/endpoints/updateStreamInfo.js @@ -0,0 +1,30 @@ +import handleStreamInfoUpdate from "../services/handleStreamInfoUpdate" + +export default { + method: "POST", + route: "/stream/info", + middlewares: ["withAuthentication"], + fn: async (req, res) => { + const { title, description, category, thumbnail } = req.body + + const info = await handleStreamInfoUpdate({ + user_id: req.user._id.toString(), + title, + description, + category, + thumbnail + }).catch((err) => { + console.error(err) + + res.status(500).json({ + error: `Cannot update info: ${err.message}`, + }) + + return null + }) + + if (info) { + return res.json(info) + } + } +} \ No newline at end of file diff --git a/packages/server/src/controllers/StreamingController/index.js b/packages/server/src/controllers/StreamingController/index.js index 93556bd8..68ccc0fe 100755 --- a/packages/server/src/controllers/StreamingController/index.js +++ b/packages/server/src/controllers/StreamingController/index.js @@ -1,391 +1,39 @@ import { Controller } from "linebridge/dist/server" -import { nanoid } from "nanoid" -import lodash from "lodash" -import axios from "axios" - -import { Schematized } from "../../lib" -import { User, StreamingKey, StreamingInfo, StreamingCategory } from "../../models" - -const streamingIngestServer = process.env.STREAMING_INGEST_SERVER ?? "" -const streamingServerAPIAddress = process.env.STREAMING_API_SERVER ?? "" - -const streamingServerAPIUri = `${streamingServerAPIAddress.startsWith("https") ? "https" : "http"}://${streamingServerAPIAddress.split("://")[1]}` - -const FILTER_KEYS = ["stream"] +import generateEndpointsFromDir from "linebridge/dist/server/lib/generateEndpointsFromDir" export default class StreamingController extends Controller { + static refName = "StreamingController" static useRoute = "/tv" - methods = { - genereteKey: async (user_id) => { - // this will generate a new key for the user - // if the user already has a key, it will be regenerated - - // get username from user_id - const userData = await User.findById(user_id) - - const streamingKey = new StreamingKey({ - user_id, - username: userData.username, - key: nanoid() - }) - - await streamingKey.save() - - return streamingKey - }, - fetchStreams: async () => { - // fetch all streams from api - let { data } = await axios.get(`${streamingServerAPIUri}/api/v1/streams`).catch((err) => { - console.error(err) - return false - }) - - let streamings = [] - - if (!data) return streamings - - streamings = data.streams - - streamings = streamings.map(async (stream) => { - const { video, audio, clients } = stream - - stream = await this.methods.generateStreamFromStreamkey(stream.name) - - let info = await StreamingInfo.findOne({ - user_id: stream.user_id - }) - - if (info) { - stream.info = info.toObject() - - stream.info.category = await StreamingCategory.findOne({ - key: stream.info.category - }) - } - - stream.video = video - stream.audio = audio - stream.connectedClients = clients ?? 0 - - return stream - }) - - streamings = await Promise.all(streamings) - - return streamings.map((stream) => { - return lodash.omit(stream, FILTER_KEYS) - }) - }, - generateStreamFromStreamkey: async (streamKey) => { - // generate a stream from a streamkey - const streamingKey = await StreamingKey.findOne({ - key: streamKey - }) - - if (!streamingKey) return false - - const streaming = { - user_id: streamingKey.user_id, - username: streamingKey.username, - sources: { - rtmp: `${streamingIngestServer}/live/${streamingKey.username}`, - hls: `${streamingServerAPIAddress}/live/${streamingKey.username}/src.m3u8`, - flv: `${streamingServerAPIAddress}/live/${streamingKey.username}/src.flv`, - } - } - - return streaming - }, - handleInfoUpdate: async (payload) => { - let info = await StreamingInfo.findOne({ - user_id: payload.user_id - }).catch((err) => { - return false - }) - - const payloadValues = { - title: payload.title, - description: payload.description, - category: payload.category, - thumbnail: payload.thumbnail, - } - - if (!info) { - // create new info - info = new StreamingInfo({ - user_id: payload.user_id, - ...payloadValues - }) - } - - // merge data - info = lodash.merge(info, { - title: payload.title, - description: payload.description, - category: payload.category, - thumbnail: payload.thumbnail, - }) - - await info.save() - - global.wsInterface.io.emit(`streaming.info_update.${payload.user_id}`, info) - - return info - } - } - - get = { - "/streaming/categories": async (req, res) => { - const categories = await StreamingCategory.find() - - return res.json(categories) - }, - "/streams": async (req, res) => { - const remoteStreams = await this.methods.fetchStreams() - - return res.json(remoteStreams) - }, - "/stream/info": { - middleware: ["withAuthentication"], - fn: async (req, res) => { - let user_id = req.query.user_id - - if (!req.query.username && !req.query.user_id) { - return res.status(400).json({ - error: "Invalid request, missing username" - }) - } - - if (!user_id) { - user_id = await User.findOne({ - username: req.query.username, - }) - - user_id = user_id["_id"].toString() - } - - let info = await StreamingInfo.findOne({ - user_id, - }) - - if (!info) { - info = new StreamingInfo({ - user_id, - }) - - await info.save() - } - - const category = await StreamingCategory.findOne({ - key: info.category - }).catch((err) => { - console.error(err) - return {} - }) ?? {} - - return res.json({ - ...info.toObject(), - ["category"]: { - key: category?.key ?? "unknown", - label: category?.label ?? "Unknown", - } - }) - } - }, - "/streaming/addresses": { - middlewares: ["withOptionalAuthentication"], - fn: async (req, res) => { - const addresses = { - api: streamingServerAPIAddress, - ingest: streamingIngestServer, - } - - if (req.user) { - addresses.liveURL = `${addresses.api}/live/${req.user.username}` - addresses.ingestURL = `${addresses.ingest}/${req.user.username}` - - addresses.hlsURL = `${addresses.liveURL}/src.m3u8` - addresses.flvURL = `${addresses.liveURL}/src.flv` - } - - return res.json(addresses) - } - }, - "/streaming/:username": async (req, res) => { - const { username } = req.params - - const streamings = await this.methods.fetchStreams() - - // search on this.streamings - const streaming = streamings.find((streaming) => streaming.username === username) - - if (streaming) { - return res.json(lodash.omit(streaming, FILTER_KEYS)) - } - - return res.status(404).json({ - error: "Stream not found" - }) - }, - "/streaming_key": { - middlewares: ["withAuthentication"], - fn: async (req, res) => { - let streamingKey = await StreamingKey.findOne({ - user_id: req.user._id.toString() - }) - - if (!streamingKey) { - const newKey = await this.methods.genereteKey(req.user._id.toString()).catch(err => { - res.status(500).json({ - error: `Cannot generate a new key: ${err.message}`, - }) - - return false - }) - - if (!newKey) { - return false - } - - return res.json(newKey) - } else { - return res.json(streamingKey) - } - } - }, - } - - put = { - "/streaming/category": { - middlewares: ["withAuthentication", "onlyAdmin"], - fn: Schematized({ - required: ["key", "label"] - }, async (req, res) => { - const { key, label } = req.selection - - const existingCategory = await StreamingCategory.findOne({ - key - }) - - if (existingCategory) { - return res.status(400).json({ - error: "Category already exists" - }) - } - - const category = new StreamingCategory({ - key, - label, - }) - - await category.save() - - return res.json(category) - }) - } - } - - post = { - "/streaming/update_info": { - middlewares: ["withAuthentication"], - fn: async (req, res) => { - const { title, description, category, thumbnail } = req.body - - const info = await this.methods.handleInfoUpdate({ - user_id: req.user._id.toString(), - title, - description, - category, - thumbnail - }).catch((err) => { - console.error(err) - - res.status(500).json({ - error: `Cannot update info: ${err.message}`, - }) - - return null - }) - - if (info) { - return res.json(info) - } - } - }, - "/streaming/publish": async (req, res) => { - const { stream } = req.body - - const streaming = await this.methods.generateStreamFromStreamkey(stream).catch((err) => { - console.error(err) - - res.status(500).json({ - error: `Cannot generate stream: ${err.message}`, - }) - - return null - }) - - if (streaming) { - global.wsInterface.io.emit(`streaming.new`, streaming) - - global.wsInterface.io.emit(`streaming.new.${streaming.username}`, streaming) - - return res.json({ - code: 0, - status: "ok" - }) - } - }, - "/streaming/unpublish": async (req, res) => { - const { stream } = req.body - - const streaming = await this.methods.generateStreamFromStreamkey(stream).catch((err) => { - console.error(err) - - return null - }) - - if (streaming) { - global.wsInterface.io.emit(`streaming.end`, streaming) - - global.wsInterface.io.emit(`streaming.end.${streaming.username}`, streaming) - - return res.json({ - code: 0, - status: "ok" - }) - } - }, - "/regenerate_streaming_key": { - middlewares: ["withAuthentication"], - fn: async (req, res) => { - // check if the user already has a key - let streamingKey = await StreamingKey.findOne({ - user_id: req.user._id.toString() - }) - - // if exists, delete it - - if (streamingKey) { - await streamingKey.remove() - } - - // generate a new key - const newKey = await this.methods.genereteKey(req.user._id.toString()).catch(err => { - res.status(500).json({ - error: `Cannot generate a new key: ${err.message}`, - }) - - return false - }) - - if (!newKey) { - return false - } - - return res.json(newKey) - } - } - } + httpEndpoints = generateEndpointsFromDir(__dirname + "/endpoints") + + // put = { + // "/streaming/category": { + // middlewares: ["withAuthentication", "onlyAdmin"], + // fn: Schematized({ + // required: ["key", "label"] + // }, async (req, res) => { + // const { key, label } = req.selection + + // const existingCategory = await StreamingCategory.findOne({ + // key + // }) + + // if (existingCategory) { + // return res.status(400).json({ + // error: "Category already exists" + // }) + // } + + // const category = new StreamingCategory({ + // key, + // label, + // }) + + // await category.save() + + // return res.json(category) + // }) + // } + // } } \ No newline at end of file diff --git a/packages/server/src/controllers/StreamingController/services/fetchStreamsFromAPI.js b/packages/server/src/controllers/StreamingController/services/fetchStreamsFromAPI.js new file mode 100644 index 00000000..7e13c855 --- /dev/null +++ b/packages/server/src/controllers/StreamingController/services/fetchStreamsFromAPI.js @@ -0,0 +1,55 @@ +import axios from "axios" +import lodash from "lodash" + +import { StreamingCategory } from "@models" +import generateStreamDataFromStreamingKey from "./generateStreamDataFromStreamingKey" + +const streamingServerAPIAddress = process.env.STREAMING_API_SERVER ?? "" + +const streamingServerAPIUri = `${streamingServerAPIAddress.startsWith("https") ? "https" : "http"}://${streamingServerAPIAddress.split("://")[1]}` + +const FILTER_KEYS = ["stream"] + +export default async () => { + // fetch all streams from api + let { data } = await axios.get(`${streamingServerAPIUri}/api/v1/streams`).catch((err) => { + console.error(err) + return false + }) + + let streamings = [] + + if (!data) return streamings + + streamings = data.streams + + streamings = streamings.map(async (stream) => { + const { video, audio, clients } = stream + + stream = await generateStreamDataFromStreamingKey(stream.name) + + let info = await StreamingInfo.findOne({ + user_id: stream.user_id + }) + + if (info) { + stream.info = info.toObject() + + stream.info.category = await StreamingCategory.findOne({ + key: stream.info.category + }) + } + + stream.video = video + stream.audio = audio + stream.connectedClients = clients ?? 0 + + return stream + }) + + streamings = await Promise.all(streamings) + + return streamings.map((stream) => { + return lodash.omit(stream, FILTER_KEYS) + }) +} \ No newline at end of file diff --git a/packages/server/src/controllers/StreamingController/services/generateStreamDataFromStreamingKey.js b/packages/server/src/controllers/StreamingController/services/generateStreamDataFromStreamingKey.js new file mode 100644 index 00000000..85233cef --- /dev/null +++ b/packages/server/src/controllers/StreamingController/services/generateStreamDataFromStreamingKey.js @@ -0,0 +1,22 @@ +import { StreamingKey } from "@models" + +export default async (key) => { + // generate a stream from a streamkey + const streamingKey = await StreamingKey.findOne({ + key: key + }) + + if (!streamingKey) return false + + const streaming = { + user_id: streamingKey.user_id, + username: streamingKey.username, + sources: { + rtmp: `${streamingIngestServer}/live/${streamingKey.username}`, + hls: `${streamingServerAPIAddress}/live/${streamingKey.username}/src.m3u8`, + flv: `${streamingServerAPIAddress}/live/${streamingKey.username}/src.flv`, + } + } + + return streaming +} \ No newline at end of file diff --git a/packages/server/src/controllers/StreamingController/services/generateStreamingKey.js b/packages/server/src/controllers/StreamingController/services/generateStreamingKey.js new file mode 100644 index 00000000..26585e8d --- /dev/null +++ b/packages/server/src/controllers/StreamingController/services/generateStreamingKey.js @@ -0,0 +1,21 @@ +import { nanoid } from "nanoid" + +import { StreamingKey, User } from "@models" + +export default async (user_id) => { + // this will generate a new key for the user + // if the user already has a key, it will be regenerated + + // get username from user_id + const userData = await User.findById(user_id) + + const streamingKey = new StreamingKey({ + user_id, + username: userData.username, + key: nanoid() + }) + + await streamingKey.save() + + return streamingKey +} \ No newline at end of file diff --git a/packages/server/src/controllers/StreamingController/services/handleStreamInfoUpdate.js b/packages/server/src/controllers/StreamingController/services/handleStreamInfoUpdate.js new file mode 100644 index 00000000..99ea74a8 --- /dev/null +++ b/packages/server/src/controllers/StreamingController/services/handleStreamInfoUpdate.js @@ -0,0 +1,40 @@ +import { StreamingInfo } from "@models" + +import lodash from "lodash" + +export default async (payload) => { + let info = await StreamingInfo.findOne({ + user_id: payload.user_id + }).catch((err) => { + return false + }) + + const payloadValues = { + title: payload.title, + description: payload.description, + category: payload.category, + thumbnail: payload.thumbnail, + } + + if (!info) { + // create new info + info = new StreamingInfo({ + user_id: payload.user_id, + ...payloadValues + }) + } + + // merge data + info = lodash.merge(info, { + title: payload.title, + description: payload.description, + category: payload.category, + thumbnail: payload.thumbnail, + }) + + await info.save() + + global.wsInterface.io.emit(`streaming.info_update.${payload.user_id}`, info) + + return info +} \ No newline at end of file diff --git a/packages/server/src/controllers/SyncController/index.js b/packages/server/src/controllers/SyncController/index.js index f7be77fd..8d0e8d4c 100755 --- a/packages/server/src/controllers/SyncController/index.js +++ b/packages/server/src/controllers/SyncController/index.js @@ -4,107 +4,109 @@ import SecureSyncEntry from "./classes/secureSyncEntry" import axios from "axios" export default class SyncController extends Controller { + static refName = "SyncController" static useRoute = "/sync" - post = { - "/spotify/auth": { - middlewares: ["withAuthentication"], - fn: async (req, res) => { - const { code, redirect_uri } = req.body + httpEndpoints = { + post: { + "/spotify/auth": { + middlewares: ["withAuthentication"], + fn: async (req, res) => { + const { code, redirect_uri } = req.body - if (!code) { - return res.status(400).json({ - message: "Missing code", - }) - } - - if (!redirect_uri) { - return res.status(400).json({ - message: "Missing redirect_uri", - }) - } - - const response = await axios({ - url: "https://accounts.spotify.com/api/token", - method: "post", - params: { - grant_type: "authorization_code", - code: code, - redirect_uri: redirect_uri - }, - headers: { - 'Authorization': `Basic ${(Buffer.from(process.env.SPOTIFY_CLIENT_ID + ":" + process.env.SPOTIFY_CLIENT_SECRET).toString("base64"))}`, - 'Content-Type': 'application/x-www-form-urlencoded' + if (!code) { + return res.status(400).json({ + message: "Missing code", + }) } - }) - if (!response) { - return res.status(400).json({ - message: "Missing data", + if (!redirect_uri) { + return res.status(400).json({ + message: "Missing redirect_uri", + }) + } + + const response = await axios({ + url: "https://accounts.spotify.com/api/token", + method: "post", + params: { + grant_type: "authorization_code", + code: code, + redirect_uri: redirect_uri + }, + headers: { + "Authorization": `Basic ${(Buffer.from(process.env.SPOTIFY_CLIENT_ID + ":" + process.env.SPOTIFY_CLIENT_SECRET).toString("base64"))}`, + "Content-Type": "application/x-www-form-urlencoded" + } }) - } - await SecureSyncEntry.set(req.user._id.toString(), "spotify_access_token", response.data.access_token) - await SecureSyncEntry.set(req.user._id.toString(), "spotify_refresh_token", response.data.refresh_token) + if (!response) { + return res.status(400).json({ + message: "Missing data", + }) + } - return res.json({ - message: "ok" - }) - } - }, - "/spotify/unlink": { - middlewares: ["withAuthentication"], - fn: async (req, res) => { - await SecureSyncEntry.delete(req.user._id.toString(), "spotify_access_token", "") - await SecureSyncEntry.delete(req.user._id.toString(), "spotify_refresh_token", "") + await SecureSyncEntry.set(req.user._id.toString(), "spotify_access_token", response.data.access_token) + await SecureSyncEntry.set(req.user._id.toString(), "spotify_refresh_token", response.data.refresh_token) - return res.json({ - message: "ok" - }) - } - } - } - - get = { - "/spotify/client_id": async (req, res) => { - return res.json({ - client_id: process.env.SPOTIFY_CLIENT_ID, - }) - }, - "/spotify/is_authorized": { - middlewares: ["withAuthentication"], - fn: async (req, res) => { - const user_id = req.user._id.toString() - const authToken = await SecureSyncEntry.get(user_id, "spotify_access_token") - - if (!authToken) { return res.json({ - is_authorized: false, + message: "ok" }) } + }, + "/spotify/unlink": { + middlewares: ["withAuthentication"], + fn: async (req, res) => { + await SecureSyncEntry.delete(req.user._id.toString(), "spotify_access_token", "") + await SecureSyncEntry.delete(req.user._id.toString(), "spotify_refresh_token", "") - return res.json({ - is_authorized: true, - }) + return res.json({ + message: "ok" + }) + } } }, - "/spotify/data": { - middlewares: ["withAuthentication"], - fn: async (req, res) => { - const user_id = req.user._id.toString() - const authToken = await SecureSyncEntry.get(user_id, "spotify_access_token") - if (!authToken) { - return res.status(400).json({ - message: "Missing auth token", + get: { + "/spotify/client_id": async (req, res) => { + return res.json({ + client_id: process.env.SPOTIFY_CLIENT_ID, + }) + }, + "/spotify/is_authorized": { + middlewares: ["withAuthentication"], + fn: async (req, res) => { + const user_id = req.user._id.toString() + const authToken = await SecureSyncEntry.get(user_id, "spotify_access_token") + + if (!authToken) { + return res.json({ + is_authorized: false, + }) + } + + return res.json({ + is_authorized: true, }) } + }, + "/spotify/data": { + middlewares: ["withAuthentication"], + fn: async (req, res) => { + const user_id = req.user._id.toString() + const authToken = await SecureSyncEntry.get(user_id, "spotify_access_token") - const response = await axios.get("https://api.spotify.com/v1/me", { - headers: { - "Authorization": `Bearer ${authToken}` - }, - }).catch((error) => { + if (!authToken) { + return res.status(400).json({ + message: "Missing auth token", + }) + } + + const response = await axios.get("https://api.spotify.com/v1/me", { + headers: { + "Authorization": `Bearer ${authToken}` + }, + }).catch((error) => { console.error(error.response.data) res.status(error.response.status).json(error.response.data) @@ -112,39 +114,40 @@ export default class SyncController extends Controller { return null }) - if (response) { - return res.json(response.data) + if (response) { + return res.json(response.data) + } + } + }, + "/spotify/currently_playing": { + middlewares: ["withAuthentication"], + fn: async (req, res) => { + const user_id = req.user._id.toString() + const authToken = await SecureSyncEntry.get(user_id, "spotify_access_token") + + if (!authToken) { + return res.status(400).json({ + message: "Missing auth token", + }) + } + + const response = await axios.get("https://api.spotify.com/v1/me/player", { + headers: { + "Authorization": `Bearer ${authToken}` + }, + }).catch((error) => { + console.error(error.response.data) + + res.status(error.response.status).json(error.response.data) + + return null + }) + + if (response) { + return res.json(response.data) + } } } }, - "/spotify/currently_playing": { - middlewares: ["withAuthentication"], - fn: async (req, res) => { - const user_id = req.user._id.toString() - const authToken = await SecureSyncEntry.get(user_id, "spotify_access_token") - - if (!authToken) { - return res.status(400).json({ - message: "Missing auth token", - }) - } - - const response = await axios.get("https://api.spotify.com/v1/me/player", { - headers: { - "Authorization": `Bearer ${authToken}` - }, - }).catch((error) => { - console.error(error.response.data) - - res.status(error.response.status).json(error.response.data) - - return null - }) - - if (response) { - return res.json(response.data) - } - } - } } } \ No newline at end of file diff --git a/packages/server/src/controllers/UserController/endpoints/checkUsernameAvailable.js b/packages/server/src/controllers/UserController/endpoints/checkUsernameAvailable.js new file mode 100644 index 00000000..7f912f61 --- /dev/null +++ b/packages/server/src/controllers/UserController/endpoints/checkUsernameAvailable.js @@ -0,0 +1,15 @@ +import { User } from "@models" + +export default { + method: "GET", + route: "/username_available", + fn: async (req, res) => { + const user = await User.findOne({ + username: req.query.username, + }) + + return res.json({ + available: !user, + }) + } +} \ No newline at end of file diff --git a/packages/server/src/controllers/UserController/endpoints/getConnectedFollowingUsers.js b/packages/server/src/controllers/UserController/endpoints/getConnectedFollowingUsers.js new file mode 100644 index 00000000..286599bd --- /dev/null +++ b/packages/server/src/controllers/UserController/endpoints/getConnectedFollowingUsers.js @@ -0,0 +1,14 @@ +import getConnectedUsersFollowing from "../methods/getConnectedUsersFollowing" + +export default { + method: "GET", + route: "/connected_following_users", + middlewares: ["withAuthentication"], + fn: async (req, res) => { + const users = await getConnectedUsersFollowing({ + from_user_id: req.user._id.toString(), + }) + + return res.json(users) + } +} \ No newline at end of file diff --git a/packages/server/src/controllers/UserController/endpoints/getPublicData.js b/packages/server/src/controllers/UserController/endpoints/getPublicData.js new file mode 100644 index 00000000..fcb43a52 --- /dev/null +++ b/packages/server/src/controllers/UserController/endpoints/getPublicData.js @@ -0,0 +1,39 @@ +import lodash from "lodash" +import { User } from "@models" + +const AllowedAnonPublicGetters = [ + "_id", + "username", + "fullName", + "avatar", + "roles" +] + +export default { + method: "GET", + route: "/public_data", + middlewares: ["withOptionalAuthentication"], + fn: async (req, res) => { + let user = req.query?.username ?? req.user.username + + if (!user) { + return res.status(400).json({ + error: "No user provided", + }) + } + + user = await User.findOne({ + username: user, + }).catch(() => null) + + if (!user) { + return res.json({ + user: null, + }) + } + + user = lodash.pick(user, AllowedAnonPublicGetters) + + return res.json(user) + } +} \ No newline at end of file diff --git a/packages/server/src/controllers/UserController/endpoints/getUserData.js b/packages/server/src/controllers/UserController/endpoints/getUserData.js new file mode 100644 index 00000000..dce8c3d9 --- /dev/null +++ b/packages/server/src/controllers/UserController/endpoints/getUserData.js @@ -0,0 +1,14 @@ +export default { + method: "GET", + route: "/:user_id/data", + middlewares: ["withAuthentication"], + fn: async (req, res) => { + let user = await User.findOne(req.params.user_id) + + if (!user) { + return res.status(404).json({ error: "User not exists" }) + } + + return res.json(user) + } +} \ No newline at end of file diff --git a/packages/server/src/controllers/UserController/endpoints/getUsersData.js b/packages/server/src/controllers/UserController/endpoints/getUsersData.js new file mode 100644 index 00000000..e2f437ae --- /dev/null +++ b/packages/server/src/controllers/UserController/endpoints/getUsersData.js @@ -0,0 +1,69 @@ +import { Schematized } from "@lib" +import { User } from "@models" + +export default { + method: "GET", + route: "/users/data", + 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) + }) +} \ No newline at end of file diff --git a/packages/server/src/controllers/UserController/endpoints/selfDeletePublicName.js b/packages/server/src/controllers/UserController/endpoints/selfDeletePublicName.js new file mode 100644 index 00000000..862a6bb2 --- /dev/null +++ b/packages/server/src/controllers/UserController/endpoints/selfDeletePublicName.js @@ -0,0 +1,27 @@ +import UpdateUserData from "../methods/updateUserData" + +export default { + method: "DELETE", + route: "/self/public_name", + middlewares: ["withAuthentication"], + fn: async (req, res) => { + const user_id = req.user._id.toString() + + UpdateUserData.update({ + user_id: user_id, + update: { + fullName: undefined + } + }) + .then((user) => { + return res.json({ + ...user + }) + }) + .catch((err) => { + return res.json(500).json({ + error: err.message + }) + }) + }, +} \ No newline at end of file diff --git a/packages/server/src/controllers/UserController/endpoints/selfUpdateData.js b/packages/server/src/controllers/UserController/endpoints/selfUpdateData.js new file mode 100644 index 00000000..a0fdbe41 --- /dev/null +++ b/packages/server/src/controllers/UserController/endpoints/selfUpdateData.js @@ -0,0 +1,60 @@ +import { Schematized } from "@lib" + +import UpdateUserData from "../methods/updateUserData" + +const AllowedPublicUpdateFields = [ + "fullName", + "avatar", + "email", + "cover", + "description", +] + +const MaxStringsLengths = { + fullName: 120, + email: 320, + description: 2000, +} + +export default { + method: "POST", + route: "/self/update_data", + middlewares: ["withAuthentication", "roles"], + fn: Schematized({ + required: ["update"], + select: ["update"], + }, async (req, res) => { + const user_id = req.user._id.toString() + + let update = {} + + AllowedPublicUpdateFields.forEach((key) => { + if (typeof req.selection.update[key] !== "undefined") { + // sanitize update + // check maximung strings length + if (typeof req.selection.update[key] === "string" && MaxStringsLengths[key]) { + if (req.selection.update[key].length > MaxStringsLengths[key]) { + // create a substring + req.selection.update[key] = req.selection.update[key].substring(0, MaxStringsLengths[key]) + } + } + + update[key] = req.selection.update[key] + } + }) + + UpdateUserData.update({ + user_id: user_id, + update: update, + }).then((user) => { + return res.json({ + ...user + }) + }) + .catch((err) => { + return res.json(500).json({ + error: err.message + }) + }) + }), +} \ No newline at end of file diff --git a/packages/server/src/controllers/UserController/endpoints/selfUpdatePassword.js b/packages/server/src/controllers/UserController/endpoints/selfUpdatePassword.js new file mode 100644 index 00000000..70ae4aff --- /dev/null +++ b/packages/server/src/controllers/UserController/endpoints/selfUpdatePassword.js @@ -0,0 +1,41 @@ +import { Schematized } from "@lib" +import { User } from "@models" + +import updateUserPassword from "../methods/updateUserPassword" +import bcrypt from "bcrypt" + +export default { + method: "POST", + route: "/self/update_password", + middlewares: ["withAuthentication"], + fn: Schematized({ + required: ["currentPassword", "newPassword"], + select: ["currentPassword", "newPassword",] + }, async (req, res) => { + const user = await User.findById(req.user._id).select("+password") + + if (!user) { + return res.status(404).json({ message: "User not found" }) + } + + const isPasswordValid = await bcrypt.compareSync(req.selection.currentPassword, user.password) + + if (!isPasswordValid) { + return res.status(401).json({ + message: "Current password dont match" + }) + } + + const result = await updateUserPassword({ + user_id: req.user._id, + password: req.selection.newPassword, + }).catch((error) => { + res.status(500).json({ message: error.message }) + return null + }) + + if (result) { + return res.json(result) + } + }) +} \ No newline at end of file diff --git a/packages/server/src/controllers/UserController/index.js b/packages/server/src/controllers/UserController/index.js index b7f91418..9da88c1e 100755 --- a/packages/server/src/controllers/UserController/index.js +++ b/packages/server/src/controllers/UserController/index.js @@ -1,369 +1,9 @@ import { Controller } from "linebridge/dist/server" -import passport from "passport" -import lodash from "lodash" -import bcrypt from "bcrypt" - -import SessionController from "../SessionController" - -import { User } from "../../models" -import { Token, Schematized } from "../../lib" - -import createUser from "./methods/createUser" -import updatePassword from "./methods/updatePassword" -import getConnectedUsersFollowing from "./methods/getConnectedUsersFollowing" - -const AllowedPublicUpdateFields = [ - "fullName", - "avatar", - "email", - "cover", - "description", -] - -const AllowedAnonPublicGetters = [ - "_id", - "username", - "fullName", - "avatar", - "roles" -] - -const MaxStringsLengths = { - fullName: 120, - email: 320, - description: 2000, -} +import generateEndpointsFromDir from "linebridge/dist/server/lib/generateEndpointsFromDir" export default class UserController extends Controller { static refName = "UserController" + static useRoute = "/user" - methods = { - createNew: async (payload) => { - const user = await createUser(payload) - - // maybe for the future can implement a event listener for this - - return user - }, - update: async (payload) => { - if (typeof payload.user_id === "undefined") { - throw new Error("No user_id provided") - } - if (typeof payload.update === "undefined") { - throw new Error("No update provided") - } - - let user = await User.findById(payload.user_id) - - if (!user) { - throw new Error("User not found") - } - - const updateKeys = Object.keys(payload.update) - - updateKeys.forEach((key) => { - user[key] = payload.update[key] - }) - - await user.save() - - global.wsInterface.io.emit(`user.update`, { - ...user.toObject(), - }) - global.wsInterface.io.emit(`user.update.${payload.user_id}`, { - ...user.toObject(), - }) - - return user.toObject() - }, - } - - get = { - "/user/public_data": { - middlewares: ["withOptionalAuthentication"], - fn: async (req, res) => { - let user = req.query?.username ?? req.user.username - - if (!user) { - return res.status(400).json({ - error: "No user provided", - }) - } - - user = await User.findOne({ - username: user, - }).catch(() => null) - - if (!user) { - return res.json({ - user: null, - }) - } - - user = lodash.pick(user, AllowedAnonPublicGetters) - - return res.json(user) - } - }, - "/self": { - middlewares: ["withAuthentication"], - fn: async (req, res) => { - return res.json(req.user) - }, - }, - "/connected_users_following": { - middlewares: ["withAuthentication"], - fn: async (req, res) => { - const users = await getConnectedUsersFollowing({ - from_user_id: req.user._id.toString(), - }) - - return res.json(users) - } - }, - "/user/username-available": async (req, res) => { - const user = await User.findOne({ - username: req.query.username, - }) - - return res.json({ - available: !user, - }) - }, - "/user/email-available": async (req, res) => { - const user = await User.findOne({ - email: req.query.email, - }) - - return res.json({ - available: !user, - }) - }, - "/user": { - middlewares: ["withAuthentication"], - fn: Schematized({ - select: ["_id", "username"], - }, async (req, res) => { - let 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": Schematized({ - required: ["username", "email", "password"], - select: ["username", "email", "password", "fullName"], - }, async (req, res) => { - const result = await this.methods.createNew(req.selection).catch((err) => { - return res.status(500).json(err.message) - }) - - return res.json(result) - }), - "/self/update_password": { - middlewares: ["withAuthentication"], - fn: Schematized({ - required: ["currentPassword", "newPassword"], - select: ["currentPassword", "newPassword",] - }, async (req, res) => { - const user = await User.findById(req.user._id).select("+password") - - if (!user) { - return res.status(404).json({ message: "User not found" }) - } - - const isPasswordValid = await bcrypt.compareSync(req.selection.currentPassword, user.password) - - if (!isPasswordValid) { - return res.status(401).json({ - message: "Current password dont match" - }) - } - - const result = await updatePassword({ - user_id: req.user._id, - password: req.selection.newPassword, - }).catch((error) => { - res.status(500).json({ message: error.message }) - return null - }) - - if (result) { - return res.json(result) - } - }) - }, - "/update_user": { - middlewares: ["withAuthentication", "roles"], - fn: Schematized({ - required: ["_id", "update"], - select: ["_id", "update"], - }, async (req, res) => { - if (!req.selection.user_id) { - req.selection.user_id = req.user._id.toString() - } - - if ((req.selection.user_id !== req.user._id.toString()) && (req.hasRole("admin") === false)) { - return res.status(403).json({ error: "You are not allowed to update this user" }) - } - - let update = {} - - AllowedPublicUpdateFields.forEach((key) => { - if (typeof req.selection.update[key] !== "undefined") { - // sanitize update - // check maximung strings length - if (typeof req.selection.update[key] === "string" && MaxStringsLengths[key]) { - if (req.selection.update[key].length > MaxStringsLengths[key]) { - // create a substring - req.selection.update[key] = req.selection.update[key].substring(0, MaxStringsLengths[key]) - } - } - - update[key] = req.selection.update[key] - } - }) - - this.methods.update({ - user_id: req.selection.user_id, - update: update, - }).then((user) => { - return res.json({ - ...user - }) - }) - .catch((err) => { - return res.json(500).json({ - error: err.message - }) - }) - }), - }, - "/unset_public_name": { - middlewares: ["withAuthentication"], - fn: Schematized({ - select: ["user_id", "roles"], - }, async (req, res) => { - if (!req.selection.user_id) { - req.selection.user_id = req.user._id.toString() - } - - if ((req.selection.user_id !== req.user._id.toString()) && (req.hasRole("admin") === false)) { - return res.status(403).json({ error: "You are not allowed to update this user" }) - } - - this.methods.update({ - user_id: req.selection.user_id, - update: { - fullName: undefined - } - }).then((user) => { - return res.json({ - ...user - }) - }) - .catch((err) => { - return res.json(500).json({ - error: err.message - }) - }) - }) - } - } + httpEndpoints = generateEndpointsFromDir(__dirname + "/endpoints") } \ No newline at end of file diff --git a/packages/server/src/controllers/UserController/methods/createUser.js b/packages/server/src/controllers/UserController/methods/createUser.js index b6d95c39..cd5f3fe7 100755 --- a/packages/server/src/controllers/UserController/methods/createUser.js +++ b/packages/server/src/controllers/UserController/methods/createUser.js @@ -1,4 +1,4 @@ -import { User } from "../../../models" +import { User } from "@models" import Avatars from "dicebar_lib" import bcrypt from "bcrypt" diff --git a/packages/server/src/controllers/UserController/methods/getConnectedUsersFollowing.js b/packages/server/src/controllers/UserController/methods/getConnectedUsersFollowing.js index a9b41acf..ea7fe388 100755 --- a/packages/server/src/controllers/UserController/methods/getConnectedUsersFollowing.js +++ b/packages/server/src/controllers/UserController/methods/getConnectedUsersFollowing.js @@ -1,4 +1,4 @@ -import { UserFollow } from "../../../models" +import { UserFollow } from "@models" export default async (payload = {}) => { const { from_user_id } = payload diff --git a/packages/server/src/controllers/UserController/methods/updateUserData.js b/packages/server/src/controllers/UserController/methods/updateUserData.js new file mode 100644 index 00000000..ec886b33 --- /dev/null +++ b/packages/server/src/controllers/UserController/methods/updateUserData.js @@ -0,0 +1,33 @@ +import { User } from "@models" + +export default async (payload) => { + if (typeof payload.user_id === "undefined") { + throw new Error("No user_id provided") + } + if (typeof payload.update === "undefined") { + throw new Error("No update provided") + } + + let user = await User.findById(payload.user_id) + + if (!user) { + throw new Error("User not found") + } + + const updateKeys = Object.keys(payload.update) + + updateKeys.forEach((key) => { + user[key] = payload.update[key] + }) + + await user.save() + + global.wsInterface.io.emit(`user.update`, { + ...user.toObject(), + }) + global.wsInterface.io.emit(`user.update.${payload.user_id}`, { + ...user.toObject(), + }) + + return user.toObject() +} \ No newline at end of file diff --git a/packages/server/src/controllers/UserController/methods/updatePassword.js b/packages/server/src/controllers/UserController/methods/updateUserPassword.js similarity index 94% rename from packages/server/src/controllers/UserController/methods/updatePassword.js rename to packages/server/src/controllers/UserController/methods/updateUserPassword.js index 69c1f044..187179c3 100755 --- a/packages/server/src/controllers/UserController/methods/updatePassword.js +++ b/packages/server/src/controllers/UserController/methods/updateUserPassword.js @@ -1,5 +1,5 @@ +import { User } from "@models" import bcrypt from "bcrypt" -import { User } from "../../../models" export default async function (payload) { const { user_id, password } = payload diff --git a/packages/server/src/controllers/index.js b/packages/server/src/controllers/index.js old mode 100755 new mode 100644 index a6948a6e..7cb1cdd7 --- a/packages/server/src/controllers/index.js +++ b/packages/server/src/controllers/index.js @@ -1,16 +1,21 @@ -export { default as ConfigController } from "./ConfigController" -export { default as RolesController } from "./RolesController" -export { default as SessionController } from "./SessionController" -export { default as UserController } from "./UserController" -export { default as FollowerController } from "./FollowerController" -export { default as FilesController } from "./FilesController" export { default as PublicController } from "./PublicController" + +export { default as UserController } from "./UserController" +export { default as AuthController } from "./AuthController" +export { default as SessionController } from "./SessionController" + +export { default as FollowController } from "./FollowController" + export { default as PostsController } from "./PostsController" -export { default as StreamingController } from "./StreamingController" -export { default as BadgesController } from "./BadgesController" export { default as CommentsController } from "./CommentsController" -export { default as SearchController } from "./SearchController" -export { default as FeaturedEventsController } from "./FeaturedEventsController" -export { default as PlaylistsController } from "./PlaylistsController" -export { default as FeedController } from "./FeedController" -export { default as SyncController } from "./SyncController" \ No newline at end of file +export { default as FeedController } from "./FeedController" // Needs to migrate to lb 0.15 + +export { default as StreamingController } from "./StreamingController" + +export { default as BadgesController } from "./BadgesController" +export { default as FeaturedEventsController } from "./FeaturedEventsController" // Needs to migrate to lb 0.15 + +export { default as PlaylistsController } from "./PlaylistsController" // Needs to migrate to lb 0.15 +export { default as FilesController } from "./FilesController" // Needs to migrate to lb 0.15 +export { default as RolesController } from "./RolesController" // Needs to migrate to lb 0.15 +export { default as SearchController } from "./SearchController" // Needs to migrate to lb 0.15 \ No newline at end of file