diff --git a/src/server/controllers/album-artists.controller.ts b/src/server/controllers/album-artists.controller.ts index b66ceff01..0ad1acb0e 100644 --- a/src/server/controllers/album-artists.controller.ts +++ b/src/server/controllers/album-artists.controller.ts @@ -1,41 +1,45 @@ import { Request, Response } from 'express'; -import { z } from 'zod'; -import { albumArtistsService } from '../services'; -import { - getSuccessResponse, - idValidation, - paginationValidation, - validateRequest, -} from '../utils'; - -const getAlbumArtists = async (req: Request, res: Response) => { - validateRequest(req, { - query: z.object({ - ...paginationValidation, - serverFolderIds: z.string().min(1), - }), - }); +import { ApiSuccess, getSuccessResponse } from '@/utils'; +import { service } from '@services/index'; +import { validation, TypedRequest } from '@validations/index'; +const getList = async (req: Request, res: Response) => { const { take, skip, serverFolderIds } = req.query; - const data = await albumArtistsService.findMany(req, { + const albumArtists = await service.albumArtists.findMany(req, { serverFolderIds: String(serverFolderIds), skip: Number(skip), take: Number(take), - user: req.auth, + user: req.authUser, }); - return res.status(data.statusCode).json(getSuccessResponse(data)); + const success = ApiSuccess.ok({ + data: albumArtists.data, + paginationItems: { + skip: Number(skip), + take: Number(take), + totalEntries: albumArtists.totalEntries, + url: req.originalUrl, + }, + }); + + return res.status(success.statusCode).json(getSuccessResponse(success)); }; -const getAlbumArtistById = async (req: Request, res: Response) => { - validateRequest(req, { params: z.object({ ...idValidation }) }); - +const getDetail = async ( + req: TypedRequest, + res: Response +) => { const { id } = req.params; - const data = await albumArtistsService.findById({ - id: Number(id), - user: req.auth, + const albumArtist = await service.albumArtists.findById({ + id, + user: req.authUser, }); - return res.status(data.statusCode).json(getSuccessResponse(data)); + + const success = ApiSuccess.ok({ data: albumArtist }); + return res.status(success.statusCode).json(getSuccessResponse(success)); }; -export const albumArtistsController = { getAlbumArtistById, getAlbumArtists }; +export const albumArtistsController = { + getDetail, + getList, +}; diff --git a/src/server/controllers/albums.controller.ts b/src/server/controllers/albums.controller.ts index 557c2fe43..8959a790c 100644 --- a/src/server/controllers/albums.controller.ts +++ b/src/server/controllers/albums.controller.ts @@ -1,60 +1,102 @@ -import { Request, Response } from 'express'; -import { z } from 'zod'; -import { AlbumSort } from '../helpers/albums.helpers'; -import { albumsService } from '../services'; -import { SortOrder } from '../types/types'; -import { - getSuccessResponse, - idValidation, - paginationValidation, - validateRequest, -} from '../utils'; +import { Response } from 'express'; +import { ApiSuccess, getSuccessResponse } from '@/utils'; +import { toApiModel } from '@helpers/api-model'; +import { service } from '@services/index'; +import { TypedRequest, validation } from '@validations/index'; -const getAlbumById = async (req: Request, res: Response) => { - validateRequest(req, { - params: z.object({ ...idValidation }), - query: z.object({ serverUrls: z.optional(z.string().min(1)) }), +const getDetail = async ( + req: TypedRequest, + res: Response +) => { + const { albumId } = req.params; + + const album = await service.albums.findById(req.authUser, { id: albumId }); + + const success = ApiSuccess.ok({ + data: toApiModel.albums({ items: [album], user: req.authUser })[0], }); - const { id } = req.params; - const { serverUrls } = req.query; - const data = await albumsService.findById({ - id: Number(id), - serverUrls: serverUrls && String(serverUrls), - user: req.auth, - }); - - return res.status(data.statusCode).json(getSuccessResponse(data)); + return res.status(success.statusCode).json(getSuccessResponse(success)); }; -const getAlbums = async (req: Request, res: Response) => { - validateRequest(req, { - query: z.object({ - ...paginationValidation, - orderBy: z.nativeEnum(SortOrder), - serverFolderIds: z.optional(z.string().min(1)), - serverUrls: z.optional(z.string().min(1)), - sortBy: z.nativeEnum(AlbumSort), - }), - }); +const getList = async ( + req: TypedRequest, + res: Response +) => { + const { serverId } = req.params; + const { take, skip, serverUrlId } = req.query; - const { take, serverFolderIds, serverUrls, sortBy, orderBy, skip } = - req.query; - - const data = await albumsService.findMany(req, { - orderBy: orderBy as SortOrder, - serverFolderIds: serverFolderIds && String(serverFolderIds), - serverUrls: serverUrls && String(serverUrls), + const albums = await service.albums.findMany({ + ...req.query, + serverId, skip: Number(skip), - sortBy: sortBy as AlbumSort, take: Number(take), - user: req.auth, + user: req.authUser, }); - return res.status(data.statusCode).json(getSuccessResponse(data)); + const serverUrl = serverUrlId + ? await service.servers.findServerUrlById({ + id: serverUrlId, + }) + : undefined; + + const success = ApiSuccess.ok({ + data: toApiModel.albums({ + items: albums.data, + serverUrl: serverUrl?.url, + user: req.authUser, + }), + paginationItems: { + skip: Number(skip), + take: Number(take), + totalEntries: albums.totalEntries, + url: req.originalUrl, + }, + }); + + return res.status(success.statusCode).json(getSuccessResponse(success)); +}; + +const getDetailSongList = async ( + req: TypedRequest, + res: Response +) => { + const { serverId } = req.params; + const { take, skip, serverUrlId } = req.query; + + const albums = await service.albums.findMany({ + ...req.query, + serverId, + skip: Number(skip), + take: Number(take), + user: req.authUser, + }); + + const serverUrl = serverUrlId + ? await service.servers.findServerUrlById({ + id: serverUrlId, + }) + : undefined; + + const success = ApiSuccess.ok({ + data: toApiModel.albums({ + items: albums.data, + serverUrl: serverUrl?.url, + user: req.authUser, + }), + paginationItems: { + skip: Number(skip), + take: Number(take), + totalEntries: albums.totalEntries, + url: req.originalUrl, + }, + }); + + return res.status(success.statusCode).json(getSuccessResponse(success)); }; export const albumsController = { - getAlbumById, - getAlbums, + getDetail, + getDetailSongList, + getList, }; diff --git a/src/server/controllers/artists.controller.ts b/src/server/controllers/artists.controller.ts index 36acc955c..edc7b966c 100644 --- a/src/server/controllers/artists.controller.ts +++ b/src/server/controllers/artists.controller.ts @@ -1,41 +1,50 @@ -import { Request, Response } from 'express'; -import { z } from 'zod'; -import { artistsService } from '../services'; -import { - getSuccessResponse, - idValidation, - paginationValidation, - validateRequest, -} from '../utils'; - -const getArtistById = async (req: Request, res: Response) => { - validateRequest(req, { params: z.object({ ...idValidation }) }); +import { Response } from 'express'; +import { ApiSuccess, getSuccessResponse } from '@/utils'; +import { service } from '@services/index'; +import { validation, TypedRequest } from '@validations/index'; +const getDetail = async ( + req: TypedRequest, + res: Response +) => { const { id } = req.params; - const data = await artistsService.findById({ - id: Number(id), - user: req.auth, + + const artist = await service.artists.findById({ + id, + user: req.authUser, }); - return res.status(data.statusCode).json(getSuccessResponse(data)); + + const success = ApiSuccess.ok({ data: artist }); + return res.status(success.statusCode).json(getSuccessResponse(success)); }; -const getArtists = async (req: Request, res: Response) => { - validateRequest(req, { - query: z.object({ - ...paginationValidation, - serverFolderIds: z.string().min(1), - }), - }); +const getList = async ( + req: TypedRequest, + res: Response +) => { + const { take, skip, serverFolderId } = req.query; - const { take, skip, serverFolderIds } = req.query; - const data = await artistsService.findMany(req, { - serverFolderIds: String(serverFolderIds), - skip: Number(skip), - take: Number(take), - user: req.auth, - }); + // const artists = await service.artists.findMany(req, { + // serverFolderIds: String(serverFolderIds), + // skip: Number(skip), + // take: Number(take), + // user: req.authUser, + // }); - return res.status(data.statusCode).json(getSuccessResponse(data)); + // const success = ApiSuccess.ok({ + // data: artists, + // paginationItems: { + // skip: Number(skip), + // take: Number(take), + // totalEntries, + // url: req.originalUrl, + // }, + // }); + + // return res.status(success.statusCode).json(getSuccessResponse(success)); }; -export const artistsController = { getArtistById, getArtists }; +export const artistsController = { + getDetail, + getList, +}; diff --git a/src/server/controllers/auth.controller.ts b/src/server/controllers/auth.controller.ts index ac1b604e9..d6423ac05 100644 --- a/src/server/controllers/auth.controller.ts +++ b/src/server/controllers/auth.controller.ts @@ -1,38 +1,42 @@ import { Request, Response } from 'express'; -import { z } from 'zod'; +import { ApiSuccess, getSuccessResponse } from '@/utils'; +import { toApiModel } from '@helpers/api-model'; +import { service } from '@services/index'; +import { validation, TypedRequest } from '@validations/index'; import packageJson from '../package.json'; -import { authService } from '../services'; -import { getSuccessResponse, validateRequest } from '../utils'; - -const login = async (req: Request, res: Response) => { - validateRequest(req, { body: z.object({ username: z.string() }) }); +const login = async ( + req: TypedRequest, + res: Response +) => { const { username } = req.body; - const { statusCode, data } = await authService.login({ username }); + const user = await service.auth.login({ username }); - return res.status(statusCode).json(getSuccessResponse({ data, statusCode })); + const success = ApiSuccess.ok({ data: toApiModel.users([user])[0] }); + return res.status(success.statusCode).json(getSuccessResponse(success)); }; -const register = async (req: Request, res: Response) => { - validateRequest(req, { - body: z.object({ - password: z.string().min(6).max(255), - username: z.string().min(4).max(26), - }), - }); - +const register = async ( + req: TypedRequest, + res: Response +) => { const { username, password } = req.body; - const { statusCode, data } = await authService.register({ + const user = await service.auth.register({ password, username, }); - return res.status(statusCode).json(getSuccessResponse({ data, statusCode })); + const success = ApiSuccess.ok({ data: toApiModel.users([user])[0] }); + return res.status(success.statusCode).json(getSuccessResponse(success)); }; const logout = async (req: Request, res: Response) => { - const { statusCode, data } = await authService.logout({ user: req.auth }); - return res.status(statusCode).json(getSuccessResponse({ data, statusCode })); + await service.auth.logout({ + user: req.authUser, + }); + + const success = ApiSuccess.noContent({ data: {} }); + return res.status(success.statusCode).json(getSuccessResponse(success)); }; const ping = async (_req: Request, res: Response) => { @@ -48,18 +52,16 @@ const ping = async (_req: Request, res: Response) => { ); }; -const refresh = async (req: Request, res: Response) => { - validateRequest(req, { - body: z.object({ - refreshToken: z.string(), - }), - }); - - const { data, statusCode } = await authService.refresh({ +const refresh = async ( + req: TypedRequest, + res: Response +) => { + const refresh = await service.auth.refresh({ refreshToken: req.body.refreshToken, }); - return res.status(statusCode).json(getSuccessResponse({ data, statusCode })); + const success = ApiSuccess.ok({ data: refresh }); + return res.status(success.statusCode).json(getSuccessResponse(success)); }; export const authController = { login, logout, ping, refresh, register }; diff --git a/src/server/controllers/index.ts b/src/server/controllers/index.ts index 9fa056308..88e2aeebf 100644 --- a/src/server/controllers/index.ts +++ b/src/server/controllers/index.ts @@ -1,6 +1,17 @@ -export * from './album-artists.controller'; -export * from './auth.controller'; -export * from './servers.controller'; -export * from './users.controller'; -export * from './artists.controller'; -export * from './albums.controller'; +import { albumArtistsController } from './album-artists.controller'; +import { albumsController } from './albums.controller'; +import { artistsController } from './artists.controller'; +import { authController } from './auth.controller'; +import { serversController } from './servers.controller'; +import { songsController } from './songs.controller'; +import { usersController } from './users.controller'; + +export const controller = { + albumArtists: albumArtistsController, + albums: albumsController, + artists: artistsController, + auth: authController, + servers: serversController, + songs: songsController, + users: usersController, +}; diff --git a/src/server/controllers/servers.controller.ts b/src/server/controllers/servers.controller.ts index 8328390e5..8d4c10c12 100644 --- a/src/server/controllers/servers.controller.ts +++ b/src/server/controllers/servers.controller.ts @@ -1,72 +1,177 @@ -import { Request, Response } from 'express'; -import { z } from 'zod'; -import { prisma } from '../lib'; -import { serversService } from '../services'; -import { getSuccessResponse, idValidation, validateRequest } from '../utils'; +import { Response } from 'express'; +import { ApiSuccess, getSuccessResponse } from '@/utils'; +import { toApiModel } from '@helpers/api-model'; +import { service } from '@services/index'; +import { TypedRequest, validation } from '@validations/index'; -const getServerById = async (req: Request, res: Response) => { - validateRequest(req, { params: z.object({ ...idValidation }) }); +const getServerDetail = async ( + req: TypedRequest, + res: Response +) => { + const { serverId } = req.params; + const data = await service.servers.findById(req.authUser, { id: serverId }); + const success = ApiSuccess.ok({ data: toApiModel.servers([data]) }); + return res.status(success.statusCode).json(getSuccessResponse(success)); +}; - const { id } = req.params; - const data = await serversService.findById(req.auth, { - id: Number(id), +const getServerList = async ( + req: TypedRequest, + res: Response +) => { + const data = await service.servers.findMany(req.authUser); + const success = ApiSuccess.ok({ data: toApiModel.servers(data) }); + return res.status(success.statusCode).json(getSuccessResponse(success)); +}; + +const deleteServer = async ( + req: TypedRequest, + res: Response +) => { + const { serverId } = req.params; + await service.servers.deleteById({ id: serverId }); + const success = ApiSuccess.noContent({ data: null }); + return res.status(success.statusCode).json(getSuccessResponse(success)); +}; + +const createServer = async ( + req: TypedRequest, + res: Response +) => { + const remoteServerLoginRes = await service.servers.remoteServerLogin( + req.body + ); + + const data = await service.servers.create({ + name: req.body.name, + ...remoteServerLoginRes, }); - return res.status(data.statusCode).json(getSuccessResponse(data)); + const success = ApiSuccess.ok({ data: toApiModel.servers([data])[0] }); + return res.status(success.statusCode).json(getSuccessResponse(success)); }; -const getServers = async (req: Request, res: Response) => { - const data = await serversService.findMany(req.auth); +const updateServer = async ( + req: TypedRequest, + res: Response +) => { + const { serverId } = req.params; + const { username, password, name, legacy, type, url } = req.body; - return res.status(data.statusCode).json(getSuccessResponse(data)); + if (type && username && password && url) { + const remoteServerLoginRes = await service.servers.remoteServerLogin({ + legacy, + password, + type, + url, + username, + }); + + const data = await service.servers.update( + { id: serverId }, + { name, ...remoteServerLoginRes } + ); + + const success = ApiSuccess.ok({ data: toApiModel.servers([data])[0] }); + return res.status(success.statusCode).json(getSuccessResponse(success)); + } + + const data = await service.servers.update({ id: serverId }, { name, url }); + const success = ApiSuccess.ok({ data: toApiModel.servers([data])[0] }); + return res.status(success.statusCode).json(getSuccessResponse(success)); }; -const createServer = async (req: Request, res: Response) => { - const data = await serversService.create(req.body); +const refreshServer = async ( + req: TypedRequest, + res: Response +) => { + const { serverId } = req.params; + const data = await service.servers.refresh({ id: serverId }); - return res.status(data.statusCode).json(getSuccessResponse(data)); + const success = ApiSuccess.ok({ data: toApiModel.servers([data])[0] }); + return res.status(success.statusCode).json(getSuccessResponse(success)); }; -const refreshServer = async (req: Request, res: Response) => { - const { id } = req.params; - const data = await serversService.refresh({ id: Number(id) }); +const scanServer = async ( + req: TypedRequest, + res: Response +) => { + const { serverId } = req.params; + const { serverFolderId } = req.body; - return res.status(data.statusCode).json(getSuccessResponse(data)); -}; - -const scanServer = async (req: Request, res: Response) => { - validateRequest(req, { - query: z.object({ serverFolderIds: z.string().optional() }), + const data = await service.servers.fullScan({ + id: serverId, + serverFolderId, }); - const { id } = req.params; - const { serverFolderIds } = req.query; - - const data = await serversService.fullScan({ - id: Number(id), - serverFolderIds: serverFolderIds && String(serverFolderIds), - userId: Number(req.auth.id), - }); - - return res.status(data.statusCode).json(getSuccessResponse(data)); + const success = ApiSuccess.ok({ data }); + return res.status(success.statusCode).json(getSuccessResponse(success)); }; -const getFolder = async (req: Request, res: Response) => { - const data = await prisma.folder.findUnique({ - include: { - children: true, - }, - where: { id: Number(req.params.id) }, +const createServerUrl = async ( + req: TypedRequest, + res: Response +) => { + const { serverId } = req.params; + const { url } = req.body; + + const data = await service.servers.createUrl({ + serverId, + url, }); - return res.status(200).json(getSuccessResponse({ data, statusCode: 200 })); + const success = ApiSuccess.ok({ data }); + return res.status(success.statusCode).json(getSuccessResponse(success)); +}; + +const deleteServerUrl = async ( + req: TypedRequest, + res: Response +) => { + const { urlId } = req.params; + + await service.servers.deleteUrlById({ + id: urlId, + }); + + const success = ApiSuccess.noContent({ data: null }); + return res.status(success.statusCode).json(getSuccessResponse(success)); +}; + +const enableServerUrl = async ( + req: TypedRequest, + res: Response +) => { + const { serverId, urlId } = req.params; + + await service.servers.enableUrlById(req.authUser, { + id: urlId, + serverId, + }); + + const success = ApiSuccess.noContent({ data: null }); + return res.status(success.statusCode).json(getSuccessResponse(success)); +}; + +const disableServerUrl = async ( + req: TypedRequest, + res: Response +) => { + await service.servers.disableUrlById(req.authUser); + + const success = ApiSuccess.noContent({ data: null }); + return res.status(success.statusCode).json(getSuccessResponse(success)); }; export const serversController = { createServer, - getFolder, - getServerById, - getServers, + createServerUrl, + deleteServer, + deleteServerUrl, + disableServerUrl, + enableServerUrl, + getServerDetail, + getServerList, refreshServer, scanServer, + updateServer, }; diff --git a/src/server/controllers/songs.controller.ts b/src/server/controllers/songs.controller.ts index 939cd173d..f4dc39334 100644 --- a/src/server/controllers/songs.controller.ts +++ b/src/server/controllers/songs.controller.ts @@ -1,35 +1,33 @@ import { Request, Response } from 'express'; -import { z } from 'zod'; -import { songsService } from '../services/songs.service'; -import { - getSuccessResponse, - paginationValidation, - validateRequest, -} from '../utils'; -const getSongs = async (req: Request, res: Response) => { - validateRequest(req, { - query: z.object({ - ...paginationValidation, - albumIds: z.optional(z.string()), - artistIds: z.optional(z.string()), - serverFolderIds: z.optional(z.string().min(1)), - songIds: z.optional(z.string()), - }), - }); +const getSongList = async (req: Request, res: Response) => { + const { serverId } = req.params; + const { take, skip, serverFolderId } = req.query; - const { take, skip, serverFolderIds } = req.query; + // const songs = await songsService.findMany(req, { + // serverFolderIds: String(serverFolderId), + // serverId, + // skip: Number(skip), + // take: Number(take), + // user: req.authUser, + // }); - const data = await songsService.findMany(req, { - serverFolderIds: String(serverFolderIds), - skip: Number(skip), - take: Number(take), - user: req.auth, - }); + // const success = ApiSuccess.ok({ + // // data: toRes.songs(songs.data, req.authUser), + // data: songs.data, + // paginationItems: { + // skip: Number(skip), + // take: Number(take), + // totalEntries: songs.totalEntries, + // url: req.originalUrl, + // }, + // }); - return res.status(data.statusCode).json(getSuccessResponse(data)); + return {}; + + // return res.status(data.statusCode).json(getSuccessResponse(data)); }; export const songsController = { - getSongs, + getSongList, }; diff --git a/src/server/controllers/users.controller.ts b/src/server/controllers/users.controller.ts index 7b3a1c1db..bbeb941d9 100644 --- a/src/server/controllers/users.controller.ts +++ b/src/server/controllers/users.controller.ts @@ -1,21 +1,22 @@ import { Request, Response } from 'express'; -import { z } from 'zod'; -import { usersService } from '../services'; -import { getSuccessResponse, idValidation, validateRequest } from '../utils'; +import { ApiSuccess, getSuccessResponse } from '@/utils'; +import { toApiModel } from '@helpers/api-model'; +import { service } from '@services/index'; -const getUser = async (req: Request, res: Response) => { - validateRequest(req, { params: z.object({ ...idValidation }) }); +const getUserDetail = async (req: Request, res: Response) => { const { id } = req.params; - const data = await usersService.getOne({ id: Number(id) }); - return res.status(data.statusCode).json(getSuccessResponse(data)); + const user = await service.users.findById(req.authUser, { id }); + const success = ApiSuccess.ok({ data: toApiModel.users([user])[0] }); + return res.status(success.statusCode).json(getSuccessResponse(success)); }; -const getUsers = async (_req: Request, res: Response) => { - const data = await usersService.getMany(); - return res.status(data.statusCode).json(getSuccessResponse(data)); +const getUserList = async (_req: Request, res: Response) => { + const users = await service.users.findMany(); + const success = ApiSuccess.ok({ data: toApiModel.users(users) }); + return res.status(success.statusCode).json(getSuccessResponse(success)); }; export const usersController = { - getUser, - getUsers, + getUserDetail, + getUserList, }; diff --git a/src/server/helpers/albums.helpers.ts b/src/server/helpers/albums.helpers.ts index d55ead09f..b1eec5d68 100644 --- a/src/server/helpers/albums.helpers.ts +++ b/src/server/helpers/albums.helpers.ts @@ -1,34 +1,40 @@ -import { Prisma } from '@prisma/client'; -import { SortOrder } from '../types/types'; -import { splitNumberString } from '../utils'; -import { songHelpers } from './songs.helpers'; +import { AuthUser } from '@/middleware'; +import { SortOrder } from '@/types/types'; +import { songHelpers } from '@helpers/songs.helpers'; export enum AlbumSort { - DATE_ADDED = 'date_added', - DATE_ADDED_REMOTE = 'date_added_remote', - DATE_RELEASED = 'date_released', + DATE_ADDED = 'added', + DATE_ADDED_REMOTE = 'addedRemote', + DATE_RELEASED = 'released', + DATE_RELEASED_YEAR = 'year', + FAVORITE = 'favorite', + NAME = 'name', RANDOM = 'random', RATING = 'rating', - TITLE = 'title', - YEAR = 'year', } -const include = (options?: { serverUrls?: string; songs: boolean }) => { - const props: Prisma.AlbumInclude = { - _count: { select: { favorites: true, songs: true } }, - albumArtist: true, - genres: true, - images: true, - ratings: true, - server: { - include: { - serverUrls: options?.serverUrls - ? { where: { id: { in: splitNumberString(options.serverUrls) } } } - : true, +const include = (options: { songs?: boolean; user?: AuthUser }) => { + // Prisma.AlbumInclude + const props = { + _count: { + select: { + favorites: true, + songs: true, }, }, - - songs: options?.songs ? songHelpers.include() : false, + albumArtists: true, + artists: true, + favorites: { where: { userId: options.user?.id } }, + genres: true, + images: true, + ratings: { + where: { + userId: options.user?.id, + }, + }, + server: true, + serverFolders: true, + songs: options?.songs && songHelpers.findMany(), }; return props; @@ -38,7 +44,7 @@ const sort = (sortBy: AlbumSort, orderBy: SortOrder) => { let order; switch (sortBy) { - case AlbumSort.TITLE: + case AlbumSort.NAME: order = { name: orderBy }; break; @@ -51,11 +57,19 @@ const sort = (sortBy: AlbumSort, orderBy: SortOrder) => { break; case AlbumSort.DATE_RELEASED: - order = { date: orderBy, year: orderBy }; + order = { releaseDate: orderBy, year: orderBy }; break; - case AlbumSort.YEAR: - order = { year: orderBy }; + case AlbumSort.DATE_RELEASED_YEAR: + order = { releaseYear: orderBy }; + break; + + case AlbumSort.RATING: + order = { rating: orderBy }; + break; + + case AlbumSort.FAVORITE: + order = { favorite: orderBy }; break; default: diff --git a/src/server/helpers/api-model.ts b/src/server/helpers/api-model.ts new file mode 100644 index 000000000..633c239e6 --- /dev/null +++ b/src/server/helpers/api-model.ts @@ -0,0 +1,520 @@ +/* eslint-disable no-underscore-dangle */ +import { + Album, + AlbumArtist, + AlbumArtistRating, + AlbumRating, + Artist, + ArtistRating, + External, + Genre, + Image, + ImageType, + Server, + ServerFolder, + ServerFolderPermission, + ServerPermission, + ServerType, + ServerUrl, + Song, + SongRating, + User, + UserServerUrl, +} from '@prisma/client'; + +const getSubsonicStreamUrl = ( + remoteId: string, + url: string, + token: string, + deviceId: string +) => { + return ( + `${url}/rest/stream.view` + + `?id=${remoteId}` + + `&${token}` + + `&v=1.13.0` + + `&c=sonixd_${deviceId}` + ); +}; + +const getJellyfinStreamUrl = ( + remoteId: string, + url: string, + token: string, + userId: string, + deviceId: string +) => { + return ( + `${url}/audio` + + `/${remoteId}/universal` + + `?userId=${userId}` + + `&audioCodec=aac` + + `&container=opus,mp3,aac,m4a,m4b,flac,wav,ogg` + + `&transcodingContainer=ts` + + `&transcodingProtocol=hls` + + `&deviceId=sonixd_${deviceId}` + + `&playSessionId=${deviceId}` + + `&api_key=${token}` + ); +}; + +const streamUrl = ( + type: ServerType, + args: { + deviceId: string; + remoteId: string; + token: string; + url: string; + userId?: string; + } +) => { + if (type === ServerType.JELLYFIN) { + return getJellyfinStreamUrl( + args.remoteId, + args.url, + args.token, + args.userId || '', + args.deviceId + ); + } + return getSubsonicStreamUrl( + args.remoteId, + args.url, + args.token, + args.deviceId + ); +}; + +const imageUrl = ( + type: ServerType, + baseUrl: string, + imageId: string, + token?: string +) => { + if (type === ServerType.JELLYFIN) { + return ( + `${baseUrl}/Items` + + `/${imageId}` + + `/Images/Primary` + + '?fillHeight=250' + + `&fillWidth=250` + + '&quality=90' + ); + } + + if (type === ServerType.SUBSONIC || type === ServerType.NAVIDROME) { + return ( + `${baseUrl}/rest/getCoverArt.view` + + `?id=${imageId}` + + `&size=250` + + `&v=1.13.0` + + `&c=sonixd` + + `&${token}` + ); + } + + return null; +}; + +const relatedAlbum = (item: Album) => { + return { + /* eslint-disable sort-keys-fix/sort-keys-fix */ + id: item.id, + name: item.name, + remoteId: item.remoteId, + deleted: item.deleted, + /* eslint-enable sort-keys-fix/sort-keys-fix */ + }; +}; + +const relatedArtists = (items: Artist[]) => { + return ( + items?.map((item) => { + return { + /* eslint-disable sort-keys-fix/sort-keys-fix */ + id: item.id, + name: item.name, + remoteId: item.remoteId, + deleted: item.deleted, + /* eslint-enable sort-keys-fix/sort-keys-fix */ + }; + }) || [] + ); +}; + +const relatedAlbumArtists = (items: AlbumArtist[]) => { + return ( + items?.map((item) => { + return { + /* eslint-disable sort-keys-fix/sort-keys-fix */ + id: item.id, + name: item.name, + remoteId: item.remoteId, + deleted: item.deleted, + /* eslint-enable sort-keys-fix/sort-keys-fix */ + }; + }) || [] + ); +}; + +const relatedGenres = (items: Genre[]) => { + return ( + items?.map((item) => { + return { + /* eslint-disable sort-keys-fix/sort-keys-fix */ + id: item.id, + name: item.name, + /* eslint-enable sort-keys-fix/sort-keys-fix */ + }; + }) || [] + ); +}; + +const relatedServerFolders = (items: ServerFolder[]) => { + const serverFolders = items?.map((item) => { + return { + /* eslint-disable sort-keys-fix/sort-keys-fix */ + id: item.id, + name: item.name, + remoteId: item.remoteId, + lastScannedAt: item.lastScannedAt, + /* eslint-enable sort-keys-fix/sort-keys-fix */ + }; + }); + + return serverFolders || []; +}; + +const relatedServerUrls = ( + items: (ServerUrl & { + userServerUrls?: UserServerUrl[]; + })[] +) => { + const serverUrls = items?.map((item) => { + const userServerUrlIds = item.userServerUrls?.map( + (userServerUrl) => userServerUrl.serverUrlId + ); + const enabled = userServerUrlIds?.some((id) => id === item.id); + + return { + /* eslint-disable sort-keys-fix/sort-keys-fix */ + id: item.id, + url: item.url, + enabled, + /* eslint-enable sort-keys-fix/sort-keys-fix */ + }; + }); + + return serverUrls || []; +}; + +const rating = ( + items: AlbumRating[] | SongRating[] | ArtistRating[] | AlbumArtistRating[] +) => { + if (items.length > 0) { + return items[0].value; + } + + return null; +}; + +const image = ( + images: Image[], + type: ServerType, + imageType: ImageType, + url: string, + remoteId: string, + token?: string +) => { + const imageRemoteUrl = images.find((i) => i.type === imageType)?.remoteUrl; + + if (!imageRemoteUrl) return null; + if (type === ServerType.JELLYFIN) { + return imageUrl(type, url, remoteId); + } + + if (type === ServerType.SUBSONIC || type === ServerType.NAVIDROME) { + return imageUrl(type, url, imageRemoteUrl, token); + } + + return null; +}; + +type DbSong = Song & DbSongInclude; + +type DbSongInclude = { + album: Album; + artists: Artist[]; + externals: External[]; + genres: Genre[]; + images: Image[]; + ratings: SongRating[]; + server: Server & { serverUrls: ServerUrl[] }; +}; + +const songs = ( + items: DbSong[], + options: { + deviceId: string; + imageUrl?: string; + serverFolderId?: number; + token: string; + type: ServerType; + url: string; + userId: string; + } +) => { + return ( + items?.map((item) => { + const url = options.url ? options.url : item.server.serverUrls[0].url; + + const stream = streamUrl(options.type, { + deviceId: options.deviceId, + remoteId: item.remoteId, + token: options.token, + url: options.url, + userId: options.userId, + }); + + return { + /* eslint-disable sort-keys-fix/sort-keys-fix */ + id: item.id, + name: item.name, + artistName: item.artistName, + album: item.album && relatedAlbum(item.album), + artists: relatedArtists(item.artists), + bitRate: item.bitRate, + container: item.container, + createdAt: item.createdAt, + deleted: item.deleted, + discNumber: item.discNumber, + duration: item.duration, + genres: relatedGenres(item.genres), + imageUrl: image( + item.images, + options.type, + ImageType.PRIMARY, + url, + item.remoteId + ), + releaseDate: item.releaseDate, + releaseYear: item.releaseYear, + remoteCreatedAt: item.remoteCreatedAt, + remoteId: item.remoteId, + // serverFolderId: item.serverFolderId, + serverId: item.serverId, + streamUrl: stream, + trackNumber: item.trackNumber, + updatedAt: item.updatedAt, + /* eslint-enable sort-keys-fix/sort-keys-fix */ + }; + }) || [] + ); +}; + +type DbAlbum = Album & DbAlbumInclude; + +type DbAlbumInclude = { + _count: { + favorites: number; + songs: number; + }; + albumArtists: AlbumArtist[]; + genres: Genre[]; + images: Image[]; + ratings: AlbumRating[]; + server: Server; + serverFolders: ServerFolder[]; + songs?: DbSong[]; +}; + +const albums = (options: { + items: DbAlbum[] | any[]; + serverUrl?: string; + user: User; +}) => { + const { items, serverUrl, user } = options; + return ( + items?.map((item) => { + const { type, token, remoteUserId } = item.server; + const url = serverUrl || item.server.url; + + return { + /* eslint-disable sort-keys-fix/sort-keys-fix */ + id: item.id, + name: item.name, + sortName: item.sortName, + releaseDate: item.releaseDate, + releaseYear: item.releaseYear, + isFavorite: item.favorites.length === 1, + rating: rating(item.ratings), + songCount: item._count.songs, + type, + imageUrl: image( + item.images, + type, + ImageType.PRIMARY, + url, + item.remoteId + ), + backdropImageUrl: image( + item.images, + type, + ImageType.BACKDROP, + url, + item.remoteId + ), + deleted: item.deleted, + remoteId: item.remoteId, + remoteCreatedAt: item.remoteCreatedAt, + createdAt: item.createdAt, + updatedAt: item.updatedAt, + genres: item.genres ? relatedGenres(item.genres) : [], + albumArtists: item.albumArtists + ? relatedAlbumArtists(item.albumArtists) + : [], + artists: item.artists ? relatedArtists(item.artists) : [], + serverFolders: relatedServerFolders(item.serverFolders), + songs: + item.songs && + songs(item.songs, { + deviceId: user.deviceId, + token, + type, + url, + userId: remoteUserId, + }), + /* eslint-enable sort-keys-fix/sort-keys-fix */ + }; + }) || [] + ); +}; + +// const relatedServerCredentials = (items: ServerCredential[]) => { +// return ( +// items.map((item) => { +// return { +// /* eslint-disable sort-keys-fix/sort-keys-fix */ +// id: item.id, +// enabled: item.enabled, +// username: item.username, +// credential: item.credential, +// /* eslint-enable sort-keys-fix/sort-keys-fix */ +// }; +// }) || [] +// ); +// }; + +// const serverCredentials = (items: ServerCredential[]) => { +// return ( +// items.map((item) => { +// return { +// /* eslint-disable sort-keys-fix/sort-keys-fix */ +// id: item.id, +// username: item.username, +// enabled: item.enabled, +// credential: item.credential, +// createdAt: item.createdAt, +// updatedAt: item.updatedAt, +// /* eslint-enable sort-keys-fix/sort-keys-fix */ +// }; +// }) || [] +// ); +// }; + +const servers = ( + items: (Server & { + serverFolders?: ServerFolder[]; + serverUrls?: (ServerUrl & { + userServerUrls?: UserServerUrl[]; + })[]; + })[] +) => { + return ( + items.map((item) => { + return { + /* eslint-disable sort-keys-fix/sort-keys-fix */ + id: item.id, + name: item.name, + url: item.url, + type: item.type, + username: item.username, + createdAt: item.createdAt, + updatedAt: item.updatedAt, + serverFolders: + item.serverFolders && relatedServerFolders(item.serverFolders), + serverUrls: item.serverUrls && relatedServerUrls(item.serverUrls), + /* eslint-enable sort-keys-fix/sort-keys-fix */ + }; + }) || [] + ); +}; + +const relatedServerFolderPermissions = (items: ServerFolderPermission[]) => { + return items.map((item) => { + return { + /* eslint-disable sort-keys-fix/sort-keys-fix */ + id: item.id, + serverFolderId: item.serverFolderId, + createdAt: item.createdAt, + updatedAt: item.updatedAt, + /* eslint-enable sort-keys-fix/sort-keys-fix */ + }; + }); +}; + +const relatedServerPermissions = (items: ServerPermission[]) => { + return items.map((item) => { + return { + /* eslint-disable sort-keys-fix/sort-keys-fix */ + id: item.id, + type: item.type, + serverId: item.serverId, + createdAt: item.createdAt, + updatedAt: item.updatedAt, + /* eslint-enable sort-keys-fix/sort-keys-fix */ + }; + }); +}; + +const users = ( + items: (User & { + accessToken?: string; + refreshToken?: string; + serverFolderPermissions?: ServerFolderPermission[]; + serverPermissions?: ServerPermission[]; + })[] +) => { + return ( + items.map((item) => { + return { + /* eslint-disable sort-keys-fix/sort-keys-fix */ + id: item.id, + username: item.username, + accessToken: item.accessToken, + refreshToken: item.refreshToken, + enabled: item.enabled, + isAdmin: item.isAdmin, + deviceId: item.deviceId, + createdAt: item.createdAt, + updatedAt: item.updatedAt, + flatServerPermissions: + item.serverPermissions && item.serverPermissions.map((s) => s.id), + serverFolderPermissions: + item.serverFolderPermissions && + relatedServerFolderPermissions(item.serverFolderPermissions), + serverPermissions: + item.serverPermissions && + relatedServerPermissions(item.serverPermissions), + /* eslint-enable sort-keys-fix/sort-keys-fix */ + }; + }) || [] + ); +}; + +export const toApiModel = { + albums, + servers, + songs, + users, +}; diff --git a/src/server/helpers/index.ts b/src/server/helpers/index.ts new file mode 100644 index 000000000..feab25116 --- /dev/null +++ b/src/server/helpers/index.ts @@ -0,0 +1,9 @@ +import { albumHelpers } from './albums.helpers'; +import { sharedHelpers } from './shared.helpers'; +import { songHelpers } from './songs.helpers'; + +export const helpers = { + albums: albumHelpers, + shared: sharedHelpers, + songs: songHelpers, +}; diff --git a/src/server/helpers/shared.helpers.ts b/src/server/helpers/shared.helpers.ts index 9daeebcf7..01c60b4fd 100644 --- a/src/server/helpers/shared.helpers.ts +++ b/src/server/helpers/shared.helpers.ts @@ -1,11 +1,108 @@ -const serverFolderFilter = (serverFolderIds: number[]) => { - return serverFolderIds!.map((serverFolderId: number) => { - return { - serverFolders: { some: { id: { equals: Number(serverFolderId) } } }, - }; +import { AuthUser } from '@/middleware'; +import { ApiError } from '@/utils'; +import { prisma } from '@lib/prisma'; + +const checkServerPermissions = ( + user: AuthUser, + options: { serverId?: string } +) => { + const { serverId } = options; + + if (user.isAdmin || !serverId) { + return; + } + + if (serverId && !user.flatServerPermissions.includes(serverId)) { + throw ApiError.forbidden(); + } +}; + +const checkServerFolderPermissions = ( + user: AuthUser, + options: { serverFolderId?: string[] | string } +) => { + const { serverFolderId } = options; + + if (user.isAdmin || !serverFolderId) { + return; + } + + let ids: string[] = []; + if (typeof serverFolderId === 'string') { + ids = [serverFolderId]; + } else if (typeof serverFolderId === 'object') { + ids = serverFolderId; + } + + for (const id of ids) { + if (!user.flatServerFolderPermissions.includes(id)) { + throw ApiError.forbidden(''); + } + } +}; + +const getAvailableServerFolderIds = async ( + user: AuthUser, + options: { serverId: string } +) => { + const { serverId } = options; + + if (user.isAdmin) { + const serverFoldersWithAccess = await prisma.serverFolder.findMany({ + where: { serverId }, + }); + + const serverFoldersWithAccessIds = serverFoldersWithAccess.map( + (serverFolder) => serverFolder.id + ); + + return serverFoldersWithAccessIds; + } + + const serverFoldersWithAccess = await prisma.serverFolder.findMany({ + where: { + OR: [ + { + AND: [ + { + serverFolderPermissions: { + some: { userId: { equals: user.id } }, + }, + }, + ], + }, + ], + }, }); + + const serverFoldersWithAccessIds = serverFoldersWithAccess.map( + (serverFolder) => serverFolder.id + ); + + return serverFoldersWithAccessIds; +}; + +const serverFolderFilter = (serverFolderIds: string[]) => { + return { + serverFolders: { every: { id: { in: serverFolderIds } } }, + }; +}; + +const paginationParams = (options: { skip: any; take: any }) => { + const { skip, take } = options; + + return { + skip: Number(skip), + take: Number(take), + }; }; export const sharedHelpers = { + checkServerFolderPermissions, + checkServerPermissions, + getAvailableServerFolderIds, + params: { + pagination: paginationParams, + }, serverFolderFilter, }; diff --git a/src/server/helpers/songs.helpers.ts b/src/server/helpers/songs.helpers.ts index 605cd546f..14b74b15e 100644 --- a/src/server/helpers/songs.helpers.ts +++ b/src/server/helpers/songs.helpers.ts @@ -1,20 +1,45 @@ import { Prisma } from '@prisma/client'; const include = () => { - const body = { + const props: Prisma.SongInclude = { + album: true, + artists: true, + externals: true, + genres: true, + images: true, + ratings: true, + server: { + include: { serverUrls: true }, + }, + }; + + return props; +}; + +const findMany = () => { + const props: Prisma.SongFindManyArgs = { include: { album: true, artists: true, externals: true, genres: true, images: true, + ratings: true, + server: { + include: { serverUrls: true }, + }, }, - orderBy: [{ disc: Prisma.SortOrder.asc }, { track: Prisma.SortOrder.asc }], + orderBy: [ + // { albumId: Prisma.SortOrder.asc }, + { discNumber: Prisma.SortOrder.asc }, + { trackNumber: Prisma.SortOrder.asc }, + ], }; - return body; + return props; }; export const songHelpers = { + findMany, include, }; diff --git a/src/server/middleware/authenticate.ts b/src/server/middleware/authenticate.ts index 38a5c93a9..bb4ff4f8b 100644 --- a/src/server/middleware/authenticate.ts +++ b/src/server/middleware/authenticate.ts @@ -1,9 +1,4 @@ -import { - ServerCredential, - ServerFolderPermission, - ServerPermission, - User, -} from '@prisma/client'; +import { ServerFolderPermission, ServerPermission, User } from '@prisma/client'; import { NextFunction, Request, Response } from 'express'; import passport from 'passport'; @@ -55,13 +50,6 @@ export const authenticate = ( (permission: ServerPermission) => permission.serverId ); - const serverCredentials = user.serverCredentials.map( - (credential: ServerCredential) => ({ - id: credential.id, - serverId: credential.serverId, - }) - ); - const props = { createdAt: user?.createdAt, enabled: user?.enabled, @@ -70,7 +58,6 @@ export const authenticate = ( id: user?.id, isAdmin: user?.isAdmin, server: req.params.serverId, - serverCredentials, serverFolderPermissions: user?.serverFolderPermissions, serverPermissions: user?.serverPermissions, updatedAt: user?.updatedAt, diff --git a/src/server/middleware/error-handler.ts b/src/server/middleware/error-handler.ts index 9089f328f..97d72fabe 100644 --- a/src/server/middleware/error-handler.ts +++ b/src/server/middleware/error-handler.ts @@ -1,5 +1,5 @@ import { NextFunction, Request, Response } from 'express'; -import { isJsonString } from '../utils'; +import { isJsonString } from '@utils/is-json-string'; export const errorHandler = ( err: any, @@ -9,7 +9,7 @@ export const errorHandler = ( ) => { let message = ''; - const trace = err.stack.match(/at .* \(.*\)/g).map((e: string) => { + const trace = err.stack?.match(/at .* \(.*\)/g).map((e: string) => { return e.replace(/\(|\)/g, ''); }); diff --git a/src/server/routes/album-artists.route.ts b/src/server/routes/album-artists.route.ts index 594b13ff3..a38a4281e 100644 --- a/src/server/routes/album-artists.route.ts +++ b/src/server/routes/album-artists.route.ts @@ -1,8 +1,13 @@ import express, { Router } from 'express'; -import { controller } from '../controllers'; +import { controller } from '@controllers/index'; +import { validation, validateRequest } from '@validations/index'; export const router: Router = express.Router({ mergeParams: true }); -router.get('/', controller.albumArtists.getAlbumArtists); +router.get('/', controller.albumArtists.getList); -router.get('/:id', controller.albumArtists.getAlbumArtistById); +router.get( + ':serverId', + validateRequest(validation.albumArtists.detail), + controller.albumArtists.getDetail +); diff --git a/src/server/routes/albums.route.ts b/src/server/routes/albums.route.ts index f4220d52d..1e7c54809 100644 --- a/src/server/routes/albums.route.ts +++ b/src/server/routes/albums.route.ts @@ -1,8 +1,23 @@ import express, { Router } from 'express'; -import { controller } from '../controllers'; +import { controller } from '@controllers/index'; +import { validateRequest, validation } from '@validations/index'; export const router: Router = express.Router({ mergeParams: true }); -router.get('/', controller.albums.getAlbumList); +router.get( + '/', + validateRequest(validation.albums.list), + controller.albums.getList +); -router.get('/:id', controller.albums.getAlbumDetail); +router.get( + '/:albumId', + validateRequest(validation.albums.detail), + controller.albums.getDetail +); + +router.get( + '/:albumId/songs', + validateRequest(validation.albums.detail), + controller.albums.getDetailSongList +); diff --git a/src/server/routes/artists.route.ts b/src/server/routes/artists.route.ts index f4bd5a8be..5ed5f0487 100644 --- a/src/server/routes/artists.route.ts +++ b/src/server/routes/artists.route.ts @@ -1,8 +1,8 @@ import express, { Router } from 'express'; -import { controller } from '../controllers'; +import { controller } from '@controllers/index'; export const router: Router = express.Router({ mergeParams: true }); -router.get('/', controller.artists.getArtists); +router.get('/', controller.artists.getList); -router.get('/:id', controller.artists.getArtistById); +router.get(':serverId', controller.artists.getDetail); diff --git a/src/server/routes/auth.route.ts b/src/server/routes/auth.route.ts index 90013e47e..9275a3dcc 100644 --- a/src/server/routes/auth.route.ts +++ b/src/server/routes/auth.route.ts @@ -1,16 +1,30 @@ import express, { Router } from 'express'; import passport from 'passport'; -import { controller } from '../controllers'; -import { authenticate } from '../middleware'; +import { controller } from '@controllers/index'; +import { authenticate } from '@middleware/authenticate'; +import { validation, validateRequest } from '@validations/index'; export const router: Router = express.Router({ mergeParams: true }); -router.post('/login', passport.authenticate('local'), controller.auth.login); +router.post( + '/login', + validateRequest(validation.auth.login), + passport.authenticate('local'), + controller.auth.login +); -router.post('/register', controller.auth.register); +router.post( + '/register', + validateRequest(validation.auth.register), + controller.auth.register +); router.post('/logout', authenticate, controller.auth.logout); -router.post('/refresh', controller.auth.refresh); +router.post( + '/refresh', + validateRequest(validation.auth.refresh), + controller.auth.refresh +); router.get('/ping', controller.auth.ping); diff --git a/src/server/routes/index.ts b/src/server/routes/index.ts index 2c3aa08d3..3faebd4f1 100644 --- a/src/server/routes/index.ts +++ b/src/server/routes/index.ts @@ -23,7 +23,21 @@ routes.use('/api/users', usersRouter); routes.use('/api/servers', serversRouter); routes.param('serverId', (req, _res, next, serverId) => { - helpers.shared.checkServerPermissions(req.auth, { serverId }); + const { serverFolderId } = req.query as { + serverFolderId?: string[] | string; + }; + + req.authUser.serverId = serverId; + + helpers.shared.checkServerPermissions(req.authUser, { serverId }); + helpers.shared.checkServerFolderPermissions(req.authUser, { + serverFolderId, + }); + + if (typeof req.query.serverFolderId === 'string') { + req.query.serverFolderId = [req.query.serverFolderId]; + } + next(); }); diff --git a/src/server/routes/servers.route.ts b/src/server/routes/servers.route.ts index d7ebdaef4..542d714ee 100644 --- a/src/server/routes/servers.route.ts +++ b/src/server/routes/servers.route.ts @@ -1,17 +1,87 @@ import express, { Router } from 'express'; -import { controller } from '../controllers'; -import { authenticateAdmin } from '../middleware'; +import { controller } from '@controllers/index'; +import { authenticateAdmin } from '@middleware/authenticate-admin'; +import { service } from '@services/index'; +import { validateRequest, validation } from '@validations/index'; export const router: Router = express.Router({ mergeParams: true }); -router.get('/', controller.servers.getServerList); +router + .route('/') + .get( + validateRequest(validation.servers.list), + controller.servers.getServerList + ) + .post( + authenticateAdmin, + validateRequest(validation.servers.create), + controller.servers.createServer + ); -router.post('/', authenticateAdmin, controller.servers.createServer); +router + .route('/:serverId') + .get( + validateRequest(validation.servers.detail), + controller.servers.getServerDetail + ) + .patch( + authenticateAdmin, + validateRequest(validation.servers.update), + controller.servers.updateServer + ) + .delete( + authenticateAdmin, + validateRequest(validation.servers.deleteServer), + controller.servers.deleteServer + ); -router.get('/:id', controller.servers.getServerDetail); +router + .route('/:serverId/refresh') + .get( + authenticateAdmin, + validateRequest(validation.servers.refresh), + controller.servers.refreshServer + ); -router.get('/:id/refresh', authenticateAdmin, controller.servers.refreshServer); +router + .route('/:serverId/scan') + .post( + validateRequest(validation.servers.scan), + authenticateAdmin, + controller.servers.scanServer + ); -router.get('/:id/folder', authenticateAdmin, controller.servers.getFolder); +router + .route('/:serverId/url') + .post( + authenticateAdmin, + validateRequest(validation.servers.createUrl), + controller.servers.createServerUrl + ); -router.post('/:id/scan', authenticateAdmin, controller.servers.scanServer); +router.param('urlId', async (_req, _res, next, urlId) => { + await service.servers.findUrlById({ id: urlId }); + next(); +}); + +router + .route('/:serverId/url/:urlId') + .delete( + authenticateAdmin, + validateRequest(validation.servers.deleteUrl), + controller.servers.deleteServerUrl + ); + +router + .route('/:serverId/url/:urlId/enable') + .post( + validateRequest(validation.servers.enableUrl), + controller.servers.enableServerUrl + ); + +router + .route('/:serverId/url/:urlId/disable') + .post( + validateRequest(validation.servers.disableUrl), + controller.servers.disableServerUrl + ); diff --git a/src/server/routes/songs.route.ts b/src/server/routes/songs.route.ts index 948d496d0..1e76fadd8 100644 --- a/src/server/routes/songs.route.ts +++ b/src/server/routes/songs.route.ts @@ -1,6 +1,11 @@ import express, { Router } from 'express'; -import { controller } from '../controllers'; +import { validation, validateRequest } from '@validations/index'; export const router: Router = express.Router({ mergeParams: true }); -router.get('/', controller.songs.getSongs); +router.get('/', validateRequest(validation.songs.list), async (req, res) => { + // const data = await controller.songs.getSongList(req.authUser, req.query); + + return res.status(200).json({}); + // return res.status(success.statusCode).json(getSuccessResponse(success)); +}); diff --git a/src/server/routes/users.route.ts b/src/server/routes/users.route.ts index 3ccf001bb..21d6b77c7 100644 --- a/src/server/routes/users.route.ts +++ b/src/server/routes/users.route.ts @@ -1,8 +1,14 @@ import express, { Router } from 'express'; -import { controller } from '../controllers'; +import { controller } from '@controllers/index'; +import { validateRequest, validation } from '@validations/index'; +import { authenticateAdmin } from '../middleware/authenticate-admin'; export const router: Router = express.Router({ mergeParams: true }); -router.get('/', controller.users.getUsers); +router.get('/', authenticateAdmin, controller.users.getUserList); -router.get('/:id', controller.users.getUser); +router.get( + ':serverId', + validateRequest(validation.users.detail), + controller.users.getUserDetail +); diff --git a/src/server/server.ts b/src/server/server.ts index c9914af4d..4b8c5d5dc 100644 --- a/src/server/server.ts +++ b/src/server/server.ts @@ -4,8 +4,8 @@ import cors from 'cors'; import express from 'express'; import passport from 'passport'; import 'express-async-errors'; -import { errorHandler } from './middleware'; -import { routes } from './routes'; +import { errorHandler } from '@/middleware'; +import { routes } from '@routes/index'; require('./lib/passport'); diff --git a/src/server/services/album-artists.service.ts b/src/server/services/album-artists.service.ts index 7246949ba..86579747b 100644 --- a/src/server/services/album-artists.service.ts +++ b/src/server/services/album-artists.service.ts @@ -1,14 +1,11 @@ +import { User } from '@prisma/client'; import { Request } from 'express'; -import { prisma } from '../lib'; -import { OffsetPagination, User } from '../types/types'; -import { - ApiError, - ApiSuccess, - folderPermissions, - splitNumberString, -} from '../utils'; +import { OffsetPagination } from '@/types/types'; +import { ApiError } from '@/utils'; +import { prisma } from '@lib/prisma'; +import { folderPermissions } from '@utils/folder-permissions'; -const findById = async (options: { id: number; user: User }) => { +const findById = async (options: { id: string; user: User }) => { const { id, user } = options; const albumArtist = await prisma.albumArtist.findUnique({ include: { @@ -32,7 +29,7 @@ const findById = async (options: { id: number; user: User }) => { throw ApiError.forbidden(''); } - return ApiSuccess.ok({ data: albumArtist }); + return albumArtist; }; const findMany = async ( @@ -40,37 +37,29 @@ const findMany = async ( options: { serverFolderIds: string; user: User } & OffsetPagination ) => { const { user, take, serverFolderIds: rServerFolderIds, skip } = options; - const serverFolderIds = splitNumberString(rServerFolderIds); + const serverFolderIds = rServerFolderIds.split(','); if (!(await folderPermissions(serverFolderIds!, user))) { throw ApiError.forbidden(''); } - const serverFoldersFilter = serverFolderIds!.map((serverFolderId: number) => { - return { - serverFolders: { some: { id: { equals: Number(serverFolderId) } } }, - }; - }); + const serverFoldersFilter = serverFolderIds!.map((serverFolderId) => ({ + serverFolders: { some: { id: { equals: serverFolderId } } }, + })); - const totalEntries = await prisma.albumArtist.count({ - where: { OR: serverFoldersFilter }, - }); - const albumArtists = await prisma.albumArtist.findMany({ - include: { genres: true }, - skip, - take, - where: { OR: serverFoldersFilter }, - }); - - return ApiSuccess.ok({ - data: albumArtists, - paginationItems: { + const [totalEntries, albumArtists] = await prisma.$transaction([ + prisma.albumArtist.count({ + where: { OR: serverFoldersFilter }, + }), + prisma.albumArtist.findMany({ + include: { genres: true }, skip, take, - totalEntries, - url: req.originalUrl, - }, - }); + where: { OR: serverFoldersFilter }, + }), + ]); + + return { data: albumArtists, totalEntries }; }; export const albumArtistsService = { diff --git a/src/server/services/albums.service.ts b/src/server/services/albums.service.ts index fbe3f9dc2..508ba989b 100644 --- a/src/server/services/albums.service.ts +++ b/src/server/services/albums.service.ts @@ -1,30 +1,15 @@ -import { Album } from '@prisma/client'; -import { Request } from 'express'; -import { albumHelpers, AlbumSort } from '../helpers/albums.helpers'; -import { sharedHelpers } from '../helpers/shared.helpers'; -import { prisma } from '../lib'; -import { OffsetPagination, SortOrder, User } from '../types/types'; -import { - ApiError, - ApiSuccess, - folderPermissions, - getFolderPermissions, - splitNumberString, -} from '../utils'; -import { toRes } from './response'; +import { AuthUser } from '@/middleware'; +import { OffsetPagination, SortOrder } from '@/types/types'; +import { ApiError } from '@/utils'; +import { AlbumSort } from '@helpers/albums.helpers'; +import { helpers } from '@helpers/index'; +import { prisma } from '@lib/prisma'; -const findById = async (options: { - id: number; - serverUrls?: string; - user: User; -}) => { - const { id, user, serverUrls } = options; +const findById = async (user: AuthUser, options: { id: string }) => { + const { id } = options; const album = await prisma.album.findUnique({ - include: { - ...albumHelpers.include({ serverUrls, songs: true }), - serverFolders: true, - }, + include: helpers.albums.include({ songs: true }), where: { id }, }); @@ -32,105 +17,94 @@ const findById = async (options: { throw ApiError.notFound(''); } - const serverFolderIds = album.serverFolders.map( - (serverFolder) => serverFolder.id - ); + const serverFolderId = album.serverFolders.map((s) => s.id); + helpers.shared.checkServerFolderPermissions(user, { serverFolderId }); - if (!(await folderPermissions(serverFolderIds, user))) { - throw ApiError.forbidden(''); - } - - return ApiSuccess.ok({ data: toRes.albums([album], user)[0] }); + return album; }; -const findMany = async ( - req: Request, - options: { - orderBy: SortOrder; - serverFolderIds?: string; - serverUrls?: string; - sortBy: AlbumSort; - user: User; - } & OffsetPagination -) => { - const { - user, - take, - serverFolderIds: rServerFolderIds, - serverUrls, - skip, - sortBy, - orderBy, - } = options; +export type AlbumFindManyOptions = { + orderBy: SortOrder; + serverFolderId?: string[]; + serverId: string; + sortBy: AlbumSort; + user: AuthUser; +} & OffsetPagination; - const serverFolderIds = rServerFolderIds - ? splitNumberString(rServerFolderIds) - : await getFolderPermissions(user); +const findMany = async (options: AlbumFindManyOptions) => { + const { take, serverFolderId, skip, sortBy, orderBy, user, serverId } = + options; - if (!(await folderPermissions(serverFolderIds!, user))) { - throw ApiError.forbidden(''); - } - - const serverFoldersFilter = sharedHelpers.serverFolderFilter( - serverFolderIds! - ); + const serverFolderIds = + serverFolderId || + (await helpers.shared.getAvailableServerFolderIds(user, { serverId })); let totalEntries = 0; - let albums: Album[]; + let albums; if (sortBy === AlbumSort.RATING) { const [count, result] = await prisma.$transaction([ prisma.albumRating.count({ where: { - album: { OR: serverFoldersFilter }, + album: { OR: helpers.shared.serverFolderFilter(serverFolderIds) }, user: { id: user.id }, }, }), prisma.albumRating.findMany({ include: { album: { - include: { ...albumHelpers.include({ serverUrls, songs: false }) }, + include: helpers.albums.include({ songs: false, user }), }, }, orderBy: { value: orderBy }, skip, take, where: { - album: { OR: serverFoldersFilter }, + album: { OR: helpers.shared.serverFolderFilter(serverFolderIds) }, user: { id: user.id }, }, }), ]); - - albums = result.map((rating) => rating.album) as Album[]; + albums = result.map((rating) => rating.album); totalEntries = count; - } else { - const [count, result] = await prisma.$transaction([ + } else if (sortBy === AlbumSort.FAVORITE) { + [totalEntries, albums] = await prisma.$transaction([ prisma.album.count({ - where: { OR: serverFoldersFilter }, + where: { + AND: [ + helpers.shared.serverFolderFilter(serverFolderIds), + { favorites: { some: { userId: user.id } } }, + ], + }, }), prisma.album.findMany({ - include: { ...albumHelpers.include({ serverUrls, songs: false }) }, - orderBy: [{ ...albumHelpers.sort(sortBy, orderBy) }], + include: helpers.albums.include({ songs: false, user }), skip, take, - where: { OR: serverFoldersFilter }, + where: { + AND: [ + helpers.shared.serverFolderFilter(serverFolderIds), + { favorites: { some: { userId: user.id } } }, + ], + }, + }), + ]); + } else { + [totalEntries, albums] = await prisma.$transaction([ + prisma.album.count({ + where: { OR: helpers.shared.serverFolderFilter(serverFolderIds) }, + }), + prisma.album.findMany({ + include: helpers.albums.include({ songs: false, user }), + orderBy: [helpers.albums.sort(sortBy, orderBy)], + skip, + take, + where: { OR: helpers.shared.serverFolderFilter(serverFolderIds) }, }), ]); - - albums = result; - totalEntries = count; } - return ApiSuccess.ok({ - data: toRes.albums(albums, user), - paginationItems: { - skip, - take, - totalEntries, - url: req.originalUrl, - }, - }); + return { data: albums, totalEntries }; }; export const albumsService = { diff --git a/src/server/services/artists.service.ts b/src/server/services/artists.service.ts index 74ba1d834..79d4bd6a6 100644 --- a/src/server/services/artists.service.ts +++ b/src/server/services/artists.service.ts @@ -1,14 +1,10 @@ +import { User } from '@prisma/client'; import { Request } from 'express'; import { prisma } from '../lib'; -import { OffsetPagination, User } from '../types/types'; -import { - ApiError, - ApiSuccess, - folderPermissions, - splitNumberString, -} from '../utils'; +import { OffsetPagination } from '../types/types'; +import { ApiError, folderPermissions } from '../utils'; -const findById = async (options: { id: number; user: User }) => { +const findById = async (options: { id: string; user: User }) => { const { id, user } = options; const artist = await prisma.artist.findUnique({ @@ -16,19 +12,16 @@ const findById = async (options: { id: number; user: User }) => { where: { id }, }); - if (!artist) { - throw ApiError.notFound(''); - } + if (!artist) throw ApiError.notFound(''); const serverFolderIds = artist.serverFolders.map( (serverFolder) => serverFolder.id ); - if (!(await folderPermissions(serverFolderIds, user))) { + if (!(await folderPermissions(serverFolderIds, user))) throw ApiError.forbidden(''); - } - return ApiSuccess.ok({ data: artist }); + return artist; }; const findMany = async ( @@ -36,41 +29,28 @@ const findMany = async ( options: { serverFolderIds: string; user: User } & OffsetPagination ) => { const { user, skip, take, serverFolderIds: rServerFolderIds } = options; - const serverFolderIds = splitNumberString(rServerFolderIds); + const serverFolderIds = rServerFolderIds.split(','); - if (!(await folderPermissions(serverFolderIds!, user))) { + if (!(await folderPermissions(serverFolderIds!, user))) throw ApiError.forbidden(''); - } - const serverFoldersFilter = serverFolderIds!.map((serverFolderId: number) => { - return { - serverFolders: { - some: { - id: { equals: Number(serverFolderId) }, - }, - }, - }; - }); + const serverFoldersFilter = serverFolderIds!.map((serverFolderId) => ({ + serverFolders: { some: { id: { equals: serverFolderId } } }, + })); - const totalEntries = await prisma.artist.count({ - where: { OR: serverFoldersFilter }, - }); - const artists = await prisma.artist.findMany({ - include: { genres: true }, - skip, - take, - where: { OR: serverFoldersFilter }, - }); - - return ApiSuccess.ok({ - data: artists, - paginationItems: { + const [totalEntries, artists] = await prisma.$transaction([ + prisma.artist.count({ + where: { OR: serverFoldersFilter }, + }), + prisma.artist.findMany({ + include: { genres: true }, skip, take, - totalEntries, - url: req.originalUrl, - }, - }); + where: { OR: serverFoldersFilter }, + }), + ]); + + return { data: artists, totalEntries }; }; export const artistsService = { diff --git a/src/server/services/auth.service.ts b/src/server/services/auth.service.ts index fdc4cfb27..f61522499 100644 --- a/src/server/services/auth.service.ts +++ b/src/server/services/auth.service.ts @@ -8,22 +8,32 @@ import { ApiError } from '../utils/api-error'; const login = async (options: { username: string }) => { const { username } = options; - const user = await prisma.user.findUnique({ where: { username } }); + const user = await prisma.user.findUnique({ + include: { serverFolderPermissions: true, serverPermissions: true }, + where: { username }, + }); - if (user) { - const accessToken = generateToken(user.id); - const refreshToken = generateRefreshToken(user.id); - - await prisma.refreshToken.create({ - data: { token: refreshToken, userId: user.id }, - }); - - const res = { ...user, accessToken, refreshToken }; - - return ApiSuccess.ok({ data: res }); + if (!user) { + throw ApiError.notFound('The user does not exist.'); } - throw ApiError.notFound('The user does not exist.'); + const serverPermissions = user.serverPermissions.map((p) => p.id); + + const otherProperties = { + isAdmin: user.isAdmin, + serverFolderPermissions: user.serverFolderPermissions.map((p) => p.id), + serverPermissions, + username: user.username, + }; + + const accessToken = generateToken(user.id, otherProperties); + const refreshToken = generateRefreshToken(user.id, otherProperties); + + await prisma.refreshToken.create({ + data: { token: refreshToken, userId: user.id }, + }); + + return { ...user, accessToken, refreshToken }; }; const register = async (options: { password: string; username: string }) => { @@ -44,7 +54,7 @@ const register = async (options: { password: string; username: string }) => { }, }); - return ApiSuccess.ok({ data: user }); + return user; }; const logout = async (options: { user: User }) => { @@ -59,19 +69,16 @@ const logout = async (options: { user: User }) => { const refresh = async (options: { refreshToken: string }) => { const { refreshToken } = options; const user = jwt.verify(refreshToken, String(process.env.TOKEN_SECRET)); - const { id } = user as { exp: number; iat: number; id: number }; + const { id } = user as { exp: number; iat: number; id: string }; const token = await prisma.refreshToken.findUnique({ where: { token: refreshToken }, }); - if (!token) { - throw ApiError.unauthorized('Invalid refresh token.'); - } + if (!token) throw ApiError.unauthorized('Invalid refresh token.'); const newToken = generateToken(id); - - return ApiSuccess.ok({ data: { accessToken: newToken } }); + return { accessToken: newToken }; }; export const authService = { diff --git a/src/server/services/index.ts b/src/server/services/index.ts index 3130596fe..f5aabd89a 100644 --- a/src/server/services/index.ts +++ b/src/server/services/index.ts @@ -1,6 +1,15 @@ -export * from './auth.service'; -export * from './servers.service'; -export * from './album-artists.service'; -export * from './users.service'; -export * from './artists.service'; -export * from './albums.service'; +import { albumArtistsService } from './album-artists.service'; +import { albumsService } from './albums.service'; +import { artistsService } from './artists.service'; +import { authService } from './auth.service'; +import { serversService } from './servers.service'; +import { usersService } from './users.service'; + +export const service = { + albumArtists: albumArtistsService, + albums: albumsService, + artists: artistsService, + auth: authService, + servers: serversService, + users: usersService, +}; diff --git a/src/server/services/response.ts b/src/server/services/response.ts deleted file mode 100644 index 71cef1b24..000000000 --- a/src/server/services/response.ts +++ /dev/null @@ -1,240 +0,0 @@ -/* eslint-disable no-underscore-dangle */ -import meanBy from 'lodash/meanBy'; -import { Item, Rating, User } from '../types/types'; -import { getImageUrl } from '../utils'; - -const getSubsonicStreamUrl = ( - remoteId: string, - url: string, - token: string, - deviceId: string -) => { - return ( - `${url}/rest/stream.view` + - `?id=${remoteId}` + - `&${token}` + - `&v=1.13.0` + - `&c=sonixd_${deviceId}` - ); -}; - -const getJellyfinStreamUrl = ( - remoteId: string, - url: string, - token: string, - userId: string, - deviceId: string -) => { - return ( - `${url}/audio` + - `/${remoteId}/universal` + - `?userId=${userId}` + - `&audioCodec=aac` + - `&container=opus,mp3,aac,m4a,m4b,flac,wav,ogg` + - `&transcodingContainer=ts` + - `&transcodingProtocol=hls` + - `&deviceId=sonixd_${deviceId}` + - `&playSessionId=${deviceId}` + - `&api_key=${token}` - ); -}; - -const streamUrl = ( - serverType: string, - args: { - deviceId: string; - remoteId: string; - token: string; - url: string; - userId?: string; - } -) => { - if (serverType === 'jellyfin') { - return getJellyfinStreamUrl( - args.remoteId, - args.url, - args.token, - args.userId || '', - args.deviceId - ); - } - - if (serverType === 'subsonic') { - return getSubsonicStreamUrl( - args.remoteId, - args.url, - args.token, - args.deviceId - ); - } - - return ''; -}; - -const relatedAlbum = (item: any) => { - return { - deleted: item.deleted, - id: item.id, - itemType: Item.ALBUM, - name: item.name, - remoteId: item.remoteId, - }; -}; - -const relatedArtists = (items: any[]) => { - return ( - items?.map((item: any) => { - return { - deleted: item.deleted, - id: item.id, - itemType: Item.ARTIST, - name: item.name, - remoteId: item.remoteId, - }; - }) || [] - ); -}; - -const relatedAlbumArtist = (item: any) => { - return { - deleted: item.deleted, - id: item.id, - itemType: item.ALBUMARTIST, - name: item.name, - remoteId: item.remoteId, - }; -}; -const relatedGenres = (genres: any[]) => { - return ( - genres?.map((genre) => { - return { - id: genre.id, - itemType: Item.GENRE, - name: genre.name, - }; - }) || [] - ); -}; - -const primaryImage = ( - images: any[], - serverType: string, - url: string, - remoteId: string -) => { - const primaryImageId = images.find((i: any) => i.name === 'Primary')?.url; - const image = !primaryImageId ? '' : getImageUrl(serverType, url, remoteId); - - return image; -}; - -const songs = ( - items: any[], - options: { - deviceId: string; - imageUrl?: string; - serverFolderId?: number; - serverType?: string; - token: string; - url?: string; - userId: string; - } -) => { - return ( - items?.map((item: any) => { - const serverType = options.serverType - ? options?.serverType - : item.server.serverType; - - const url = options.url ? options.url : item.server.serverUrls[0]; - - return { - album: item.album && relatedAlbum(item.album), - artistName: item.artistName, - artists: relatedArtists(item.artists), - bitRate: item.bitRate, - container: item.container, - createdAt: item.createdAt, - date: item.date, - deleted: item.deleted, - disc: item.disc, - duration: item.duration, - genres: relatedGenres(item.genres), - id: item.id, - imageUrl: - primaryImage(item.images, serverType, url, item.remoteId) || - options.imageUrl, - itemType: Item.SONG, - name: item.name, - remoteCreatedAt: item.remoteCreatedAt, - remoteId: item.remoteId, - serverFolderId: item.serverFolderId, - serverId: item.serverId, - streamUrl: streamUrl(serverType, { - deviceId: options.deviceId, - remoteId: item.remoteId, - token: options.token, - url, - userId: options.userId, - }), - track: item.track, - updatedAt: item.updatedAt, - year: item.year, - }; - }) || [] - ); -}; - -const albums = (items: any[], user: User) => { - return ( - items?.map((item: any) => { - const { serverType, token, remoteUserId } = item.server; - const { url } = item.server.serverUrls[0]; - const rating = item.ratings.find( - (r: Rating) => r.userId === user.id - )?.value; - const averageRating = meanBy(item.ratings, 'value'); - const imageUrl = primaryImage( - item.images, - serverType, - url, - item.remoteId - ); - - return { - albumArtist: item.albumArtist && relatedAlbumArtist(item.albumArtist), - averageRating, - createdAt: item.createdAt, - date: item.date, - deleted: item.deleted, - genres: relatedGenres(item.genres), - id: item.id, - imageUrl, - itemType: Item.ALBUM, - name: item.name, - rating, - remoteCreatedAt: item.remoteCreatedAt, - remoteId: item.remoteId, - serverFolderId: item.serverFolderId, - serverType, - songCount: item._count.songs, - songs: songs(item.songs, { - deviceId: user.deviceId, - imageUrl, - serverFolderId: item.serverFolderId, - serverType, - token, - url, - userId: remoteUserId, - }), - updatedAt: item.updatedAt, - year: item.year, - }; - }) || [] - ); -}; - -export const toRes = { - albums, - songs, -}; diff --git a/src/server/services/servers.service.ts b/src/server/services/servers.service.ts index 742a75c27..a8dd3ce42 100644 --- a/src/server/services/servers.service.ts +++ b/src/server/services/servers.service.ts @@ -1,27 +1,98 @@ +import { ServerType, TaskType } from '@prisma/client'; +import { SortOrder } from '@/types/types'; +import { helpers } from '../helpers'; import { prisma } from '../lib'; -import { - jellyfinApi, - jellyfinTasks, - subsonicApi, - subsonicTasks, -} from '../queue'; -import { User } from '../types/types'; -import { ApiError, ApiSuccess, splitNumberString } from '../utils'; +import { AuthUser } from '../middleware'; +import { subsonic } from '../queue'; +import { jellyfin } from '../queue/jellyfin'; +import { navidrome } from '../queue/navidrome'; +import { ApiError } from '../utils'; -const findById = async (user: User, options: { id: number }) => { +const remoteServerLogin = async (options: { + legacy?: boolean; + password: string; + type: ServerType; + url: string; + username: string; +}) => { + if (options.type === ServerType.JELLYFIN) { + const res = await jellyfin.api.authenticate({ + password: options.password, + url: options.url, + username: options.username, + }); + + if (!res) { + throw ApiError.badRequest('Invalid credentials.'); + } + + return { + remoteUserId: res.User.Id, + token: res.AccessToken, + type: ServerType.JELLYFIN, + url: options.url, + username: options.username, + }; + } + + if (options.type === ServerType.SUBSONIC) { + const res = await subsonic.api.authenticate({ + legacy: options.legacy, + password: options.password, + url: options.url, + username: options.username, + }); + + if (res.status === 'failed') { + throw ApiError.badRequest('Invalid credentials.'); + } + + return { + remoteUserId: '', + token: res.token, + type: ServerType.SUBSONIC, + url: options.url, + username: options.username, + }; + } + + if (options.type === ServerType.NAVIDROME) { + const res = await navidrome.api.authenticate({ + password: options.password, + url: options.url, + username: options.username, + }); + + return { + altToken: `u=${res.name}&s=${res.subsonicSalt}&t=${res.subsonicToken}`, + remoteUserId: res.id, + token: res.token, + type: ServerType.NAVIDROME, + url: options.url, + username: options.username, + }; + } + + throw ApiError.badRequest('Server type invalid.'); +}; + +const findById = async (user: AuthUser, options: { id: string }) => { const { id } = options; + + helpers.shared.checkServerPermissions(user, { serverId: id }); + const server = await prisma.server.findUnique({ include: { serverFolders: user.isAdmin ? true : { where: { - OR: [ - { isPublic: true }, - { serverFolderPermissions: { some: { userId: user.id } } }, - ], + OR: [{ serverFolderPermissions: { some: { userId: user.id } } }], }, }, + serverPermissions: { + where: { userId: user.id }, + }, }, where: { id }, }); @@ -30,108 +101,162 @@ const findById = async (user: User, options: { id: number }) => { throw ApiError.notFound(''); } - if (!user.isAdmin && server.serverFolders.length === 0) { - throw ApiError.forbidden(''); - } - - return ApiSuccess.ok({ data: server }); + return server; }; -const findMany = async (user: User) => { - let servers; - +const findMany = async (user: AuthUser) => { if (user.isAdmin) { - servers = await prisma.server.findMany({ - include: { serverFolders: true }, - }); - } else { - servers = await prisma.server.findMany({ + return prisma.server.findMany({ include: { serverFolders: { - where: { - OR: [ - { isPublic: true }, - { serverFolderPermissions: { some: { userId: user.id } } }, - ], + orderBy: { name: SortOrder.ASC }, + }, + serverPermissions: { + orderBy: { createdAt: SortOrder.ASC }, + where: { userId: user.id }, + }, + serverUrls: { + include: { + userServerUrls: { + where: { userId: user.id }, + }, }, }, }, - where: { serverFolders: { some: { isPublic: true } } }, + orderBy: { createdAt: SortOrder.ASC }, }); } - return ApiSuccess.ok({ data: servers }); + const servers = await prisma.server.findMany({ + include: { + serverFolders: { + orderBy: { name: SortOrder.ASC }, + where: { id: { in: user.flatServerFolderPermissions } }, + }, + serverPermissions: { + orderBy: { createdAt: SortOrder.ASC }, + where: { userId: user.id }, + }, + serverUrls: true, + }, + orderBy: { createdAt: SortOrder.ASC }, + where: { id: { in: user.flatServerPermissions } }, + }); + + return servers; }; const create = async (options: { + altToken?: string; // Used for Navidrome only name: string; remoteUserId: string; - serverType: string; token: string; + type: ServerType; url: string; username: string; }) => { - const checkDuplicate = await prisma.server.findUnique({ + const isDuplicate = await prisma.server.findUnique({ where: { url: options.url }, }); - if (checkDuplicate) { + if (isDuplicate) { throw ApiError.conflict('Server already exists.'); } - let musicFoldersData: { + const serverFolders: { name: string; remoteId: string; - serverId: number; + serverId: string; }[] = []; - if (options.serverType === 'subsonic') { - const musicFoldersRes = await subsonicApi.getMusicFolders({ + if (options.type === ServerType.SUBSONIC) { + const serverFoldersRes = await subsonic.api.getMusicFolders({ token: options.token, url: options.url, }); - if (!musicFoldersRes) { + if (!serverFoldersRes) { throw ApiError.badRequest('Server is inaccessible.'); } + const serverFoldersCreate = serverFoldersRes.map((folder) => { + return { + name: folder.name, + remoteId: String(folder.id), + }; + }); + + const server = await prisma.server.create({ + data: { + ...options, + serverFolders: { create: serverFoldersCreate }, + serverUrls: { create: { url: options.url } }, + }, + }); + + // for (const serverFolder of serverFolders) { + // await prisma.serverFolder.upsert({ + // create: serverFolder, + // update: { name: serverFolder.name }, + // where: { + // uniqueServerFolderId: { + // remoteId: serverFolder.remoteId, + // serverId: serverFolder.serverId, + // }, + // }, + // }); + // } + + return server; + } + + if (options.type === ServerType.NAVIDROME) { + const serverFoldersRes = await subsonic.api.getMusicFolders({ + token: options.altToken, + url: options.url, + }); + + if (!serverFoldersRes) { + throw ApiError.badRequest('Server is inaccessible.'); + } + + const serverFoldersCreate = serverFoldersRes.map((folder) => { + return { + name: folder.name, + remoteId: String(folder.id), + }; + }); const server = await prisma.server.create({ data: { name: options.name, remoteUserId: options.remoteUserId, - serverType: options.serverType, + serverFolders: { create: serverFoldersCreate }, + serverUrls: { create: { url: options.url } }, token: options.token, + type: options.type, url: options.url, username: options.username, }, }); - musicFoldersData = musicFoldersRes.map((musicFolder) => { - return { - name: musicFolder.name, - remoteId: String(musicFolder.id), - serverId: server.id, - }; - }); - - musicFoldersData.forEach(async (musicFolder) => { + for (const serverFolder of serverFolders) { await prisma.serverFolder.upsert({ - create: musicFolder, - update: { name: musicFolder.name }, + create: serverFolder, + update: { name: serverFolder.name }, where: { uniqueServerFolderId: { - remoteId: musicFolder.remoteId, - serverId: musicFolder.serverId, + remoteId: serverFolder.remoteId, + serverId: serverFolder.serverId, }, }, }); - }); + } - return ApiSuccess.ok({ data: { ...server } }); + return server; } - if (options.serverType === 'jellyfin') { - const musicFoldersRes = await jellyfinApi.getMusicFolders({ + if (options.type === ServerType.JELLYFIN) { + const musicFoldersRes = await jellyfin.api.getMusicFolders({ remoteUserId: options.remoteUserId, token: options.token, url: options.url, @@ -141,62 +266,72 @@ const create = async (options: { throw ApiError.badRequest('Server is inaccessible.'); } + const serverFoldersCreate = musicFoldersRes.map((musicFolder) => { + return { + name: musicFolder.Name, + remoteId: String(musicFolder.Id), + }; + }); + const server = await prisma.server.create({ data: { name: options.name, remoteUserId: options.remoteUserId, - serverType: options.serverType, + serverFolders: { create: serverFoldersCreate }, serverUrls: { create: { url: options.url } }, token: options.token, + type: options.type, url: options.url, username: options.username, }, }); - musicFoldersData = musicFoldersRes.map((musicFolder) => { - return { - name: musicFolder.Name, - remoteId: String(musicFolder.Id), - serverId: server.id, - }; - }); - - musicFoldersData.forEach(async (musicFolder) => { - await prisma.serverFolder.upsert({ - create: musicFolder, - update: { name: musicFolder.name }, - where: { - uniqueServerFolderId: { - remoteId: musicFolder.remoteId, - serverId: musicFolder.serverId, - }, - }, - }); - }); - - return ApiSuccess.ok({ data: { ...server } }); + return server; } - return ApiSuccess.ok({ data: {} }); + throw ApiError.badRequest('Server type invalid.'); }; -const refresh = async (options: { id: number }) => { - const { id } = options; - const server = await prisma.server.findUnique({ where: { id } }); +const update = async ( + options: { id: string }, + data: { + altToken?: string; // Used for Navidrome only + name?: string; + remoteUserId?: string; + token?: string; + type?: ServerType; + url?: string; + username?: string; + } +) => { + return prisma.server.update({ + data, + where: { id: options.id }, + }); +}; + +const deleteById = async (options: { id: string }) => { + return prisma.server.delete({ + where: { id: options.id }, + }); +}; + +const refresh = async (options: { id: string }) => { + const server = await prisma.server.findUnique({ where: { id: options.id } }); if (!server) { throw ApiError.notFound(''); } - let musicFoldersData: { + let serverFolders: { name: string; remoteId: string; - serverId: number; + serverId: string; }[] = []; - if (server.serverType === 'subsonic') { - const musicFoldersRes = await subsonicApi.getMusicFolders(server); - musicFoldersData = musicFoldersRes.map((musicFolder) => { + if (server.type === ServerType.SUBSONIC) { + const serverFoldersRes = await subsonic.api.getMusicFolders(server); + serverFolders = serverFoldersRes.map((musicFolder) => { return { name: musicFolder.name, remoteId: String(musicFolder.id), @@ -205,9 +340,9 @@ const refresh = async (options: { id: number }) => { }); } - if (server.serverType === 'jellyfin') { - const musicFoldersRes = await jellyfinApi.getMusicFolders(server); - musicFoldersData = musicFoldersRes.map((musicFolder) => { + if (server.type === ServerType.JELLYFIN) { + const musicFoldersRes = await jellyfin.api.getMusicFolders(server); + serverFolders = musicFoldersRes.map((musicFolder) => { return { name: musicFolder.Name, remoteId: String(musicFolder.Id), @@ -217,29 +352,24 @@ const refresh = async (options: { id: number }) => { } // mark as deleted if not found - - musicFoldersData.forEach(async (musicFolder) => { + for (const serverFolder of serverFolders) { await prisma.serverFolder.upsert({ - create: musicFolder, - update: { name: musicFolder.name }, + create: serverFolder, + update: { name: serverFolder.name }, where: { uniqueServerFolderId: { - remoteId: musicFolder.remoteId, - serverId: musicFolder.serverId, + remoteId: serverFolder.remoteId, + serverId: serverFolder.serverId, }, }, }); - }); + } - return ApiSuccess.ok({ data: { ...server } }); + return server; }; -const fullScan = async (options: { - id: number; - serverFolderIds?: string; - userId: number; -}) => { - const { id, serverFolderIds } = options; +const fullScan = async (options: { id: string; serverFolderId?: string[] }) => { + const { id, serverFolderId } = options; const server = await prisma.server.findUnique({ include: { serverFolders: true }, where: { id }, @@ -250,52 +380,183 @@ const fullScan = async (options: { } let serverFolders; - if (serverFolderIds) { - const selectedServerFolderIds = splitNumberString(serverFolderIds); - serverFolders = server.serverFolders.filter((folder) => - selectedServerFolderIds?.includes(folder.id) + if (serverFolderId) { + serverFolders = server.serverFolders.filter((f) => + serverFolderId?.includes(f.id) ); } else { serverFolders = server.serverFolders; } - if (server.serverType === 'jellyfin') { - for (const serverFolder of serverFolders) { - const task = await prisma.task.create({ - data: { - completed: false, - inProgress: true, - name: 'Full scan', - serverFolderId: serverFolder.id, - }, - }); - - await jellyfinTasks.scanAll(server, serverFolder, task); - } + if (serverFolders.length === 0) { + throw ApiError.notFound('No matching server folders found.'); } - if (server.serverType === 'subsonic') { - for (const serverFolder of serverFolders) { - const task = await prisma.task.create({ - data: { - completed: false, - inProgress: true, - name: 'Full scan', - serverFolderId: serverFolder.id, - }, - }); + const task = await prisma.task.create({ + data: { + completed: false, + name: 'Full scan', + server: { connect: { id: server.id } }, + type: TaskType.FULL_SCAN, + }, + }); - await subsonicTasks.scanAll(server, serverFolder, task); - } + if (server.type === ServerType.JELLYFIN) { + await jellyfin.scanner.scanAll(server, serverFolders, task); } - return ApiSuccess.ok({ data: {} }); + if (server.type === ServerType.SUBSONIC) { + await subsonic.scanner.scanAll(server, serverFolders, task); + } + + if (server.type === ServerType.NAVIDROME) { + await navidrome.scanner.scanAll(server, serverFolders, task); + } + + return {}; +}; + +const findServerUrlById = async (options: { id: string }) => { + const serverUrl = await prisma.serverUrl.findUnique({ + where: { id: options.id }, + }); + + return serverUrl; +}; + +// const findCredentialById = async (options: { id: string }) => { +// const credential = await prisma.serverCredential.findUnique({ +// where: { id: options.id }, +// }); + +// if (!credential) { +// throw ApiError.notFound('Credential not found.'); +// } + +// return credential; +// }; + +// const createCredential = async (options: { +// credential: string; +// serverId: string; +// userId: string; +// username: string; +// }) => { +// const { credential, serverId, userId, username } = options; + +// const serverCredential = await prisma.serverCredential.create({ +// data: { +// credential, +// serverId, +// userId, +// username, +// }, +// }); + +// return serverCredential; +// }; + +// const deleteCredentialById = async (options: { id: string }) => { +// await prisma.serverCredential.delete({ +// where: { id: options.id }, +// }); +// }; + +// const enableCredentialById = async (options: { id: string }) => { +// const serverCredential = await prisma.serverCredential.update({ +// data: { enabled: true }, +// where: { id: options.id }, +// }); + +// const { id, userId, serverId } = serverCredential; + +// await prisma.serverCredential.updateMany({ +// data: { enabled: false }, +// where: { AND: [{ serverId, userId }, { NOT: { id } }] }, +// }); + +// return serverCredential; +// }; + +// const disableCredentialById = async (options: { id: string }) => { +// const serverCredential = await prisma.serverCredential.update({ +// data: { enabled: false }, +// where: { id: options.id }, +// }); + +// return serverCredential; +// }; + +const createUrl = async (options: { serverId: string; url: string }) => { + const { serverId, url } = options; + + const serverUrl = await prisma.serverUrl.create({ + data: { + serverId, + url, + }, + }); + + return serverUrl; +}; + +const findUrlById = async (options: { id: string }) => { + const url = await prisma.serverUrl.findUnique({ + where: { id: options.id }, + }); + + if (!url) { + throw ApiError.notFound('Url not found.'); + } + + return url; +}; + +const deleteUrlById = async (options: { id: string }) => { + await prisma.serverUrl.delete({ + where: { id: options.id }, + }); + + return null; +}; + +const enableUrlById = async ( + user: AuthUser, + options: { id: string; serverId: string } +) => { + await prisma.userServerUrl.deleteMany({ where: { userId: user.id } }); + await prisma.userServerUrl.create({ + data: { + serverId: options.serverId, + serverUrlId: options.id, + userId: user.id, + }, + }); + + return null; +}; + +const disableUrlById = async (user: AuthUser) => { + await prisma.userServerUrl.deleteMany({ + where: { userId: user.id }, + }); + + return null; }; export const serversService = { create, + createUrl, + deleteById, + deleteUrlById, + disableUrlById, + enableUrlById, findById, findMany, + findServerUrlById, + findUrlById, fullScan, refresh, + remoteServerLogin, + update, }; diff --git a/src/server/services/songs.service.ts b/src/server/services/songs.service.ts index b67db4e15..edba0b02a 100644 --- a/src/server/services/songs.service.ts +++ b/src/server/services/songs.service.ts @@ -1,22 +1,18 @@ +import { User } from '@prisma/client'; import { Request } from 'express'; import { prisma } from '../lib'; -import { User } from '../types/types'; -import { - ApiError, - ApiSuccess, - folderPermissions, - splitNumberString, -} from '../utils'; -import { toRes } from './response'; +import { SortOrder } from '../types/types'; +import { ApiError, ApiSuccess, folderPermissions } from '../utils'; +// import { toRes } from './response'; import { SongRequestParams } from './types'; -const findById = async (options: { id: number; user: User }) => { - const { id, user } = options; +const findById = async (options: { id: string; user: User }) => { + const { id } = options; const album = await prisma.album.findUnique({ include: { _count: true, - albumArtist: true, + albumArtists: true, genres: true, songs: { include: { @@ -26,15 +22,16 @@ const findById = async (options: { id: number; user: User }) => { genres: true, images: true, }, - orderBy: [{ disc: 'asc' }, { track: 'asc' }], + orderBy: [ + { discNumber: SortOrder.ASC }, + { trackNumber: SortOrder.ASC }, + ], }, }, where: { id }, }); - if (!album) { - throw ApiError.notFound(''); - } + if (!album) throw ApiError.notFound(''); // if (!(await folderPermissions([album?.serverFolderId], user))) { // throw ApiError.forbidden(''); @@ -49,37 +46,37 @@ const findMany = async ( ) => { const { albumIds: rawAlbumIds, - artistIds: rawArtistIds, + // artistIds: rawArtistIds, + serverId, songIds: rawSongIds, user, skip, take, serverFolderIds: rServerFolderIds, } = options; - const serverFolderIds = splitNumberString(rServerFolderIds); - const albumIds = splitNumberString(rawAlbumIds); - const artistIds = splitNumberString(rawArtistIds); - const songIds = splitNumberString(rawSongIds); + const serverFolderIds = rServerFolderIds.split(','); + const albumIds = rawAlbumIds && rawAlbumIds.split(','); + // const artistIds = rawArtistIds && rawArtistIds.split(','); + const songIds = rawSongIds && rawSongIds.split(','); if (serverFolderIds) { - if (!(await folderPermissions(serverFolderIds, user))) { + if (!(await folderPermissions(serverFolderIds, user))) throw ApiError.forbidden(''); - } } // const serverFoldersFilter = serverFolderIds!.map((serverFolderId: number) => { // return { serverFolders: { id: { equals: serverFolderId } } }; // }); - const serverFoldersFilter = { - serverFolders: { some: { id: { in: serverFolderIds } } }, - }; + // const serverFoldersFilter = { + // serverFolders: { some: { id: { in: serverFolderIds } } }, + // }; const [totalEntries, songs] = await prisma.$transaction([ prisma.song.count({ where: { OR: [ - serverFoldersFilter, + // serverFoldersFilter, { albumId: { in: albumIds }, id: { in: songIds }, @@ -96,19 +93,16 @@ const findMany = async ( }, skip, take, - where: { OR: serverFoldersFilter }, + where: { + AND: { + // OR: serverFoldersFilter, + serverId, + }, + }, }), ]); - return ApiSuccess.ok({ - data: songs, - paginationItems: { - skip, - take, - totalEntries, - url: req.originalUrl, - }, - }); + return { data: songs, totalEntries }; }; export const songsService = { diff --git a/src/server/services/types.ts b/src/server/services/types.ts index d2f350a29..736999172 100644 --- a/src/server/services/types.ts +++ b/src/server/services/types.ts @@ -4,5 +4,6 @@ export interface SongRequestParams extends OffsetPagination { albumIds?: string; artistIds?: string; serverFolderIds: string; + serverId: string; songIds?: string; } diff --git a/src/server/services/users.service.ts b/src/server/services/users.service.ts index 1d11a78c7..db68b4916 100644 --- a/src/server/services/users.service.ts +++ b/src/server/services/users.service.ts @@ -1,39 +1,32 @@ -import { prisma, exclude } from '../lib'; -import { ApiError, ApiSuccess } from '../utils'; +import { prisma } from '../lib'; +import { AuthUser } from '../middleware'; +import { ApiError } from '../utils'; -const getOne = async (options: { id: number }) => { +const findById = async (user: AuthUser, options: { id: string }) => { const { id } = options; - const user = await prisma.user.findUnique({ - include: { - serverFolderPermissions: true, - }, + + if (!user.isAdmin && user.id !== id) { + throw ApiError.forbidden(); + } + + const uniqueUser = await prisma.user.findUnique({ + include: { serverFolderPermissions: true }, where: { id }, }); - if (!user) { + if (!uniqueUser) { throw ApiError.notFound(''); } - return ApiSuccess.ok({ data: exclude(user, 'password') }); + return uniqueUser; }; -const getMany = async () => { - const users = await prisma.user.findMany({ - select: { - createdAt: true, - enabled: true, - id: true, - isAdmin: true, - serverFolderPermissions: true, - updatedAt: true, - username: true, - }, - }); - - return ApiSuccess.ok({ data: users }); +const findMany = async () => { + const users = await prisma.user.findMany({}); + return users; }; export const usersService = { - getMany, - getOne, + findById, + findMany, }; diff --git a/src/server/utils/folder-permissions.ts b/src/server/utils/folder-permissions.ts index d689d5b8b..74f218f7e 100644 --- a/src/server/utils/folder-permissions.ts +++ b/src/server/utils/folder-permissions.ts @@ -1,5 +1,5 @@ +import { User } from '@prisma/client'; import { prisma } from '../lib'; -import { User } from '../types/types'; export enum Roles { NONE = 0, @@ -24,12 +24,8 @@ export const folderPermissions = async (serverFolderIds: any[], user: User) => { const serverFoldersWithAccess = await prisma.serverFolder.findMany({ where: { OR: [ - { - isPublic: true, - }, { AND: [ - { isPublic: false }, { serverFolderPermissions: { some: { userId: { equals: user.id } }, @@ -66,12 +62,8 @@ export const getFolderPermissions = async (user: User) => { const serverFoldersWithAccess = await prisma.serverFolder.findMany({ where: { OR: [ - { - isPublic: true, - }, { AND: [ - { isPublic: false }, { serverFolderPermissions: { some: { userId: { equals: user.id } }, diff --git a/src/server/validations/album-artists.validation.ts b/src/server/validations/album-artists.validation.ts index 283f5033b..a5bb19811 100644 --- a/src/server/validations/album-artists.validation.ts +++ b/src/server/validations/album-artists.validation.ts @@ -1,5 +1,5 @@ import { z } from 'zod'; -import { paginationValidation, idValidation } from './shared.validation'; +import { idValidation, paginationValidation } from './shared.validation'; export const list = { body: z.object({}), diff --git a/src/server/validations/albums.validation.ts b/src/server/validations/albums.validation.ts index 50ab707d3..b9f966082 100644 --- a/src/server/validations/albums.validation.ts +++ b/src/server/validations/albums.validation.ts @@ -1,5 +1,5 @@ import { z } from 'zod'; -import { AlbumSort } from '../helpers/albums.helpers'; +import { AlbumSort } from '@helpers/albums.helpers'; import { idValidation, orderByValidation, @@ -22,13 +22,32 @@ const list = { const detail = { body: z.object({}), - params: z.object({ ...idValidation('id') }), + params: z.object({ + ...idValidation('albumId'), + ...idValidation('serverId'), + }), query: z.object({ ...serverUrlIdValidation, }), }; +const detailSongList = { + body: z.object({}), + params: z.object({ + ...idValidation('albumId'), + ...idValidation('serverId'), + }), + query: z.object({ + ...paginationValidation, + ...serverFolderIdValidation, + ...orderByValidation, + ...serverUrlIdValidation, + sortBy: z.nativeEnum(AlbumSort), + }), +}; + export const albumsValidation = { detail, + detailSongList, list, }; diff --git a/src/server/validations/artists.validation.ts b/src/server/validations/artists.validation.ts index 97cf19f6c..cd7ca5089 100644 --- a/src/server/validations/artists.validation.ts +++ b/src/server/validations/artists.validation.ts @@ -1,5 +1,5 @@ import { z } from 'zod'; -import { AlbumSort } from '../helpers/albums.helpers'; +import { AlbumSort } from '@helpers/albums.helpers'; import { idValidation, orderByValidation, diff --git a/src/server/validations/servers.validation.ts b/src/server/validations/servers.validation.ts index 421b50351..c398d42dd 100644 --- a/src/server/validations/servers.validation.ts +++ b/src/server/validations/servers.validation.ts @@ -4,7 +4,32 @@ import { idValidation } from './shared.validation'; const detail = { body: z.object({}), - params: z.object({ ...idValidation('id') }), + params: z.object({ ...idValidation('serverId') }), + query: z.object({}), +}; + +const list = { + body: z.object({}), + params: z.object({}), + query: z.object({}), +}; + +const deleteServer = { + body: z.object({}), + params: z.object({ ...idValidation('serverId') }), + query: z.object({}), +}; + +const update = { + body: z.object({ + legacy: z.boolean().optional(), + name: z.string().optional(), + password: z.string().optional(), + type: z.nativeEnum(ServerType), + url: z.string().optional(), + username: z.string().optional(), + }), + params: z.object({ ...idValidation('serverId') }), query: z.object({}), }; @@ -27,26 +52,103 @@ const create = { const scan = { body: z.object({ serverFolderId: z.string().array().optional() }), - params: z.object({ ...idValidation('id') }), + params: z.object({ ...idValidation('serverId') }), query: z.object({}), }; const refresh = { body: z.object({}), - params: z.object({ ...idValidation('id') }), + params: z.object({ ...idValidation('serverId') }), query: z.object({}), }; const createCredential = { - body: z.object({ credential: z.string() }), - params: z.object({ ...idValidation('id') }), + body: z.object({ credential: z.string(), username: z.string() }), + params: z.object({ ...idValidation('serverId') }), + query: z.object({}), +}; + +const getCredentialDetail = { + body: z.object({}), + params: z.object({ ...idValidation('serverId') }), + query: z.object({}), +}; + +const deleteCredential = { + body: z.object({}), + params: z.object({ + ...idValidation('serverId'), + ...idValidation('credentialId'), + }), + query: z.object({}), +}; + +const enableCredential = { + body: z.object({}), + params: z.object({ + ...idValidation('serverId'), + ...idValidation('credentialId'), + }), + query: z.object({}), +}; + +const disableCredential = { + body: z.object({}), + params: z.object({ + ...idValidation('serverId'), + ...idValidation('credentialId'), + }), + query: z.object({}), +}; + +const createUrl = { + body: z.object({ url: z.string() }), + params: z.object({ ...idValidation('serverId') }), + query: z.object({}), +}; + +const deleteUrl = { + body: z.object({}), + params: z.object({ + ...idValidation('serverId'), + ...idValidation('urlId'), + }), + query: z.object({}), +}; + +const enableUrl = { + body: z.object({}), + params: z.object({ + ...idValidation('serverId'), + ...idValidation('urlId'), + }), + query: z.object({}), +}; + +const disableUrl = { + body: z.object({}), + params: z.object({ + ...idValidation('serverId'), + ...idValidation('urlId'), + }), query: z.object({}), }; export const serversValidation = { create, createCredential, + createUrl, + deleteCredential, + deleteServer, + deleteUrl, detail, + disableCredential, + disableUrl, + enableCredential, + enableUrl, + getCredentialDetail, + list, refresh, scan, + update, }; diff --git a/src/server/validations/shared.validation.ts b/src/server/validations/shared.validation.ts index 6483cfea9..1b4f142f8 100644 --- a/src/server/validations/shared.validation.ts +++ b/src/server/validations/shared.validation.ts @@ -1,8 +1,8 @@ -// Modified from zod-express-middleware: https://github.com/Aquila169/zod-express-middleware import { Request, RequestHandler } from 'express'; import { z, ZodError, ZodSchema } from 'zod'; -import { SortOrder } from '../types/types'; -import { ApiError } from '../utils'; +import { SortOrder } from '@/types/types'; +import { ApiError } from '@/utils'; +// Modified from zod-express-middleware: https://github.com/Aquila169/zod-express-middleware export type TypedRequest< S extends {