Move server directory outside of frontend src

This commit is contained in:
jeffvli
2022-10-25 16:52:45 -07:00
parent 863dce88b7
commit 0438f2d5f2
105 changed files with 16946 additions and 6901 deletions
+68
View File
@@ -0,0 +1,68 @@
import { User } from '@prisma/client';
import { Request } from 'express';
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: string; user: User }) => {
const { id, user } = options;
const albumArtist = await prisma.albumArtist.findUnique({
include: {
albums: { include: { songs: true } },
genres: true,
images: true,
serverFolders: true,
},
where: { id },
});
if (!albumArtist) {
throw ApiError.notFound('');
}
const serverFolderIds = albumArtist.serverFolders.map(
(serverFolder) => serverFolder.id
);
if (!(await folderPermissions(serverFolderIds, user))) {
throw ApiError.forbidden('');
}
return albumArtist;
};
const findMany = async (
req: Request,
options: { serverFolderIds: string; user: User } & OffsetPagination
) => {
const { user, take, serverFolderIds: rServerFolderIds, skip } = options;
const serverFolderIds = rServerFolderIds.split(',');
if (!(await folderPermissions(serverFolderIds!, user))) {
throw ApiError.forbidden('');
}
const serverFoldersFilter = serverFolderIds!.map((serverFolderId) => ({
serverFolders: { some: { id: { equals: serverFolderId } } },
}));
const [totalEntries, albumArtists] = await prisma.$transaction([
prisma.albumArtist.count({
where: { OR: serverFoldersFilter },
}),
prisma.albumArtist.findMany({
include: { genres: true },
skip,
take,
where: { OR: serverFoldersFilter },
}),
]);
return { data: albumArtists, totalEntries };
};
export const albumArtistsService = {
findById,
findMany,
};
+113
View File
@@ -0,0 +1,113 @@
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 (user: AuthUser, options: { id: string }) => {
const { id } = options;
const album = await prisma.album.findUnique({
include: helpers.albums.include({ songs: true }),
where: { id },
});
if (!album) {
throw ApiError.notFound('');
}
const serverFolderId = album.serverFolders.map((s) => s.id);
helpers.shared.checkServerFolderPermissions(user, { serverFolderId });
return album;
};
export type AlbumFindManyOptions = {
orderBy: SortOrder;
serverFolderId?: string[];
serverId: string;
sortBy: AlbumSort;
user: AuthUser;
} & OffsetPagination;
const findMany = async (options: AlbumFindManyOptions) => {
const { take, serverFolderId, skip, sortBy, orderBy, user, serverId } =
options;
const serverFolderIds =
serverFolderId ||
(await helpers.shared.getAvailableServerFolderIds(user, { serverId }));
let totalEntries = 0;
let albums;
if (sortBy === AlbumSort.RATING) {
const [count, result] = await prisma.$transaction([
prisma.albumRating.count({
where: {
album: { OR: helpers.shared.serverFolderFilter(serverFolderIds) },
user: { id: user.id },
},
}),
prisma.albumRating.findMany({
include: {
album: {
include: helpers.albums.include({ songs: false, user }),
},
},
orderBy: { value: orderBy },
skip,
take,
where: {
album: { OR: helpers.shared.serverFolderFilter(serverFolderIds) },
user: { id: user.id },
},
}),
]);
albums = result.map((rating) => rating.album);
totalEntries = count;
} else if (sortBy === AlbumSort.FAVORITE) {
[totalEntries, albums] = await prisma.$transaction([
prisma.album.count({
where: {
AND: [
helpers.shared.serverFolderFilter(serverFolderIds),
{ favorites: { some: { userId: user.id } } },
],
},
}),
prisma.album.findMany({
include: helpers.albums.include({ songs: false, user }),
skip,
take,
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) },
}),
]);
}
return { data: albums, totalEntries };
};
export const albumsService = {
findById,
findMany,
};
+59
View File
@@ -0,0 +1,59 @@
import { User } from '@prisma/client';
import { Request } from 'express';
import { prisma } from '../lib';
import { OffsetPagination } from '../types/types';
import { ApiError, folderPermissions } from '../utils';
const findById = async (options: { id: string; user: User }) => {
const { id, user } = options;
const artist = await prisma.artist.findUnique({
include: { genres: true, serverFolders: true },
where: { id },
});
if (!artist) throw ApiError.notFound('');
const serverFolderIds = artist.serverFolders.map(
(serverFolder) => serverFolder.id
);
if (!(await folderPermissions(serverFolderIds, user)))
throw ApiError.forbidden('');
return artist;
};
const findMany = async (
req: Request,
options: { serverFolderIds: string; user: User } & OffsetPagination
) => {
const { user, skip, take, serverFolderIds: rServerFolderIds } = options;
const serverFolderIds = rServerFolderIds.split(',');
if (!(await folderPermissions(serverFolderIds!, user)))
throw ApiError.forbidden('');
const serverFoldersFilter = serverFolderIds!.map((serverFolderId) => ({
serverFolders: { some: { id: { equals: serverFolderId } } },
}));
const [totalEntries, artists] = await prisma.$transaction([
prisma.artist.count({
where: { OR: serverFoldersFilter },
}),
prisma.artist.findMany({
include: { genres: true },
skip,
take,
where: { OR: serverFoldersFilter },
}),
]);
return { data: artists, totalEntries };
};
export const artistsService = {
findById,
findMany,
};
+89
View File
@@ -0,0 +1,89 @@
import { User } from '@prisma/client';
import bcrypt from 'bcryptjs';
import jwt from 'jsonwebtoken';
import { prisma } from '../lib';
import { generateRefreshToken, generateToken } from '../lib/passport';
import { ApiSuccess, randomString } from '../utils';
import { ApiError } from '../utils/api-error';
const login = async (options: { username: string }) => {
const { username } = options;
const user = await prisma.user.findUnique({
include: { serverFolderPermissions: true, serverPermissions: true },
where: { username },
});
if (!user) {
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 }) => {
const { username, password } = options;
const userExists = await prisma.user.findUnique({ where: { username } });
if (userExists) {
throw ApiError.conflict('The user already exists.');
}
const hashedPassword = await bcrypt.hash(password, 12);
const user = await prisma.user.create({
data: {
deviceId: `${username}_${randomString(10)}`,
enabled: false,
password: hashedPassword,
username,
},
});
return user;
};
const logout = async (options: { user: User }) => {
const { user } = options;
await prisma.refreshToken.deleteMany({
where: { userId: user.id },
});
return ApiSuccess.noContent({ data: {} });
};
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: string };
const token = await prisma.refreshToken.findUnique({
where: { token: refreshToken },
});
if (!token) throw ApiError.unauthorized('Invalid refresh token.');
const newToken = generateToken(id);
return { accessToken: newToken };
};
export const authService = {
login,
logout,
refresh,
register,
};
+15
View File
@@ -0,0 +1,15 @@
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,
};
+562
View File
@@ -0,0 +1,562 @@
import { ServerType, TaskType } from '@prisma/client';
import { SortOrder } from '@/types/types';
import { helpers } from '../helpers';
import { prisma } from '../lib';
import { AuthUser } from '../middleware';
import { subsonic } from '../queue';
import { jellyfin } from '../queue/jellyfin';
import { navidrome } from '../queue/navidrome';
import { ApiError } from '../utils';
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: [{ serverFolderPermissions: { some: { userId: user.id } } }],
},
},
serverPermissions: {
where: { userId: user.id },
},
},
where: { id },
});
if (!server) {
throw ApiError.notFound('');
}
return server;
};
const findMany = async (user: AuthUser) => {
if (user.isAdmin) {
return prisma.server.findMany({
include: {
serverFolders: {
orderBy: { name: SortOrder.ASC },
},
serverPermissions: {
orderBy: { createdAt: SortOrder.ASC },
where: { userId: user.id },
},
serverUrls: {
include: {
userServerUrls: {
where: { userId: user.id },
},
},
},
},
orderBy: { createdAt: SortOrder.ASC },
});
}
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;
token: string;
type: ServerType;
url: string;
username: string;
}) => {
const isDuplicate = await prisma.server.findUnique({
where: { url: options.url },
});
if (isDuplicate) {
throw ApiError.conflict('Server already exists.');
}
const serverFolders: {
name: string;
remoteId: string;
serverId: string;
}[] = [];
if (options.type === ServerType.SUBSONIC) {
const serverFoldersRes = await subsonic.api.getMusicFolders({
token: options.token,
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: {
...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,
serverFolders: { create: serverFoldersCreate },
serverUrls: { create: { url: options.url } },
token: options.token,
type: options.type,
url: options.url,
username: options.username,
},
});
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.JELLYFIN) {
const musicFoldersRes = await jellyfin.api.getMusicFolders({
remoteUserId: options.remoteUserId,
token: options.token,
url: options.url,
});
if (!musicFoldersRes) {
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,
serverFolders: { create: serverFoldersCreate },
serverUrls: { create: { url: options.url } },
token: options.token,
type: options.type,
url: options.url,
username: options.username,
},
});
return server;
}
throw ApiError.badRequest('Server type invalid.');
};
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 serverFolders: {
name: string;
remoteId: string;
serverId: string;
}[] = [];
if (server.type === ServerType.SUBSONIC) {
const serverFoldersRes = await subsonic.api.getMusicFolders(server);
serverFolders = serverFoldersRes.map((musicFolder) => {
return {
name: musicFolder.name,
remoteId: String(musicFolder.id),
serverId: server.id,
};
});
}
if (server.type === ServerType.JELLYFIN) {
const musicFoldersRes = await jellyfin.api.getMusicFolders(server);
serverFolders = musicFoldersRes.map((musicFolder) => {
return {
name: musicFolder.Name,
remoteId: String(musicFolder.Id),
serverId: server.id,
};
});
}
// mark as deleted if not found
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;
};
const fullScan = async (options: { id: string; serverFolderId?: string[] }) => {
const { id, serverFolderId } = options;
const server = await prisma.server.findUnique({
include: { serverFolders: true },
where: { id },
});
if (!server) {
throw ApiError.notFound('Server does not exist.');
}
let serverFolders;
if (serverFolderId) {
serverFolders = server.serverFolders.filter((f) =>
serverFolderId?.includes(f.id)
);
} else {
serverFolders = server.serverFolders;
}
if (serverFolders.length === 0) {
throw ApiError.notFound('No matching server folders found.');
}
const task = await prisma.task.create({
data: {
completed: false,
name: 'Full scan',
server: { connect: { id: server.id } },
type: TaskType.FULL_SCAN,
},
});
if (server.type === ServerType.JELLYFIN) {
await jellyfin.scanner.scanAll(server, serverFolders, task);
}
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,
};
+111
View File
@@ -0,0 +1,111 @@
import { User } from '@prisma/client';
import { Request } from 'express';
import { prisma } from '../lib';
import { SortOrder } from '../types/types';
import { ApiError, ApiSuccess, folderPermissions } from '../utils';
// import { toRes } from './response';
import { SongRequestParams } from './types';
const findById = async (options: { id: string; user: User }) => {
const { id } = options;
const album = await prisma.album.findUnique({
include: {
_count: true,
albumArtists: true,
genres: true,
songs: {
include: {
album: true,
artists: true,
externals: true,
genres: true,
images: true,
},
orderBy: [
{ discNumber: SortOrder.ASC },
{ trackNumber: SortOrder.ASC },
],
},
},
where: { id },
});
if (!album) throw ApiError.notFound('');
// if (!(await folderPermissions([album?.serverFolderId], user))) {
// throw ApiError.forbidden('');
// }
return ApiSuccess.ok({ data: album });
};
const findMany = async (
req: Request,
options: SongRequestParams & { user: User }
) => {
const {
albumIds: rawAlbumIds,
// artistIds: rawArtistIds,
serverId,
songIds: rawSongIds,
user,
skip,
take,
serverFolderIds: rServerFolderIds,
} = options;
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)))
throw ApiError.forbidden('');
}
// const serverFoldersFilter = serverFolderIds!.map((serverFolderId: number) => {
// return { serverFolders: { id: { equals: serverFolderId } } };
// });
// const serverFoldersFilter = {
// serverFolders: { some: { id: { in: serverFolderIds } } },
// };
const [totalEntries, songs] = await prisma.$transaction([
prisma.song.count({
where: {
OR: [
// serverFoldersFilter,
{
albumId: { in: albumIds },
id: { in: songIds },
},
],
},
}),
prisma.song.findMany({
include: {
_count: { select: { favorites: true } },
genres: true,
images: true,
serverFolders: { include: { server: true } },
},
skip,
take,
where: {
AND: {
// OR: serverFoldersFilter,
serverId,
},
},
}),
]);
return { data: songs, totalEntries };
};
export const songsService = {
findById,
findMany,
};
+9
View File
@@ -0,0 +1,9 @@
import { OffsetPagination } from '../types/types';
export interface SongRequestParams extends OffsetPagination {
albumIds?: string;
artistIds?: string;
serverFolderIds: string;
serverId: string;
songIds?: string;
}
+32
View File
@@ -0,0 +1,32 @@
import { prisma } from '../lib';
import { AuthUser } from '../middleware';
import { ApiError } from '../utils';
const findById = async (user: AuthUser, options: { id: string }) => {
const { id } = options;
if (!user.isAdmin && user.id !== id) {
throw ApiError.forbidden();
}
const uniqueUser = await prisma.user.findUnique({
include: { serverFolderPermissions: true },
where: { id },
});
if (!uniqueUser) {
throw ApiError.notFound('');
}
return uniqueUser;
};
const findMany = async () => {
const users = await prisma.user.findMany({});
return users;
};
export const usersService = {
findById,
findMany,
};