rewrite controller, including Endpoint class model

This commit is contained in:
SrGooglo 2023-01-26 19:39:32 +00:00
parent 02cc20cd34
commit 22aeb48634
86 changed files with 2149 additions and 2001 deletions

View File

@ -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)
}
}

View File

@ -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")
},
}

View File

@ -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)
})
}

View File

@ -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")
}

View File

@ -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
}

View File

@ -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)
}
}

View File

@ -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)
}
})
}

View File

@ -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)
}
}

View File

@ -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)
})
}

View File

@ -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)
})
}

View File

@ -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)
})
}

View File

@ -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")
}

View File

@ -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,
})
}
})
}

View File

@ -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)
}
}
}

View File

@ -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)
}
}

View File

@ -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")
}

View File

@ -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) => {
},
}
}

View File

@ -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)
}
}
},
}
}

View File

@ -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)
}
}
}

View File

@ -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)
}
}
}
}

View File

@ -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),
})
}
}

View File

@ -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)
}
}

View File

@ -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)
})
}

View File

@ -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")
}

View File

@ -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,
}
}

View File

@ -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,
}
}

View File

@ -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),
})
}),
},
}
}

View File

@ -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)
}
})
}
}
}
}

View File

@ -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)
})
}

View File

@ -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
})
}
}

View File

@ -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)
})
}

View File

@ -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)
}
}

View File

@ -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)
}
}

View File

@ -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)
})
}

View File

@ -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
})
})
}

View File

@ -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
})
}
}

View File

@ -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" })
// }
// }
// }
}

View File

@ -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)
}
}

View File

@ -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)
}
})
}

View File

@ -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)
}
})
}

View File

@ -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)
}
}

View File

@ -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")
}

View File

@ -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)
})
})
},
},
}
}

View File

@ -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)
}
}
}

View File

@ -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")
}
}

View File

@ -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",
})
},
}

View File

@ -0,0 +1,8 @@
export default {
method: "GET",
route: "/current",
middlewares: ["withAuthentication"],
fn: async (req, res) => {
return res.json(req.currentSession)
}
}

View File

@ -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)
},
}

View File

@ -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 })
},
}

View File

@ -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)
},
}

View File

@ -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")
}

View File

@ -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)
}
}

View File

@ -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",
}
})
}
}

View File

@ -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)
}
}

View File

@ -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)
}
}
}

View File

@ -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)
}
}

View File

@ -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"
})
}
}
}

View File

@ -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"
})
}
}
}

View File

@ -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)
}
}

View File

@ -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)
}
}
}

View File

@ -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)
// })
// }
// }
}

View File

@ -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)
})
}

View File

@ -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
}

View File

@ -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
}

View File

@ -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
}

View File

@ -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)
}
}
}
}
}

View File

@ -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,
})
}
}

View File

@ -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)
}
}

View File

@ -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)
}
}

View File

@ -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)
}
}

View File

@ -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)
})
}

View File

@ -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
})
})
},
}

View File

@ -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
})
})
}),
}

View File

@ -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)
}
})
}

View File

@ -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")
}

View File

@ -1,4 +1,4 @@
import { User } from "../../../models"
import { User } from "@models"
import Avatars from "dicebar_lib"
import bcrypt from "bcrypt"

View File

@ -1,4 +1,4 @@
import { UserFollow } from "../../../models"
import { UserFollow } from "@models"
export default async (payload = {}) => {
const { from_user_id } = payload

View File

@ -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()
}

View File

@ -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

31
packages/server/src/controllers/index.js Executable file → Normal file
View File

@ -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"
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