From f1a55975985a14d33f03accd68ede0147f45f509 Mon Sep 17 00:00:00 2001 From: Sander Vocke Date: Fri, 13 Nov 2020 13:09:08 +0000 Subject: [PATCH] User authentication and per-user data (#31) Add basic user registration and login support. Each user has their own private music library. Reviewed-on: https://gitea.octiron.soleus.nu/sander/MuDBase/pulls/31 --- client/src/App.tsx | 5 +- client/src/api.ts | 40 +- client/src/components/MainWindow.tsx | 72 +- client/src/components/appbar/AppBar.tsx | 84 +- .../components/querybuilder/QBLeafElem.tsx | 30 +- .../components/querybuilder/QBNodeElem.tsx | 10 +- .../querybuilder/QBSelectWithRequest.tsx | 12 +- client/src/components/tables/ResultsTable.tsx | 4 +- client/src/components/windows/Windows.tsx | 14 +- .../components/windows/album/AlbumWindow.tsx | 21 +- .../windows/artist/ArtistWindow.tsx | 23 +- .../components/windows/login/LoginWindow.tsx | 131 ++ .../windows/manage_tags/ManageTagMenu.tsx | 4 +- .../windows/manage_tags/ManageTagsWindow.tsx | 12 +- .../windows/manage_tags/NewTagMenu.tsx | 4 +- .../windows/manage_tags/TagChange.tsx | 6 +- .../components/windows/query/QueryWindow.tsx | 37 +- .../windows/register/RegisterWindow.tsx | 139 ++ .../components/windows/song/SongWindow.tsx | 20 +- .../src/components/windows/tag/TagWindow.tsx | 25 +- client/src/lib/query/Query.tsx | 12 +- client/src/lib/useAuth.tsx | 102 ++ package-lock.json | 1446 +---------------- package.json | 3 +- scripts/gpm_retrieve/gpm_retrieve.py | 108 +- server/app.ts | 140 +- ...ailsEndpointHandler.ts => AlbumDetails.ts} | 29 +- ...ilsEndpointHandler.ts => ArtistDetails.ts} | 18 +- ...AlbumEndpointHandler.ts => CreateAlbum.ts} | 6 +- ...tistEndpointHandler.ts => CreateArtist.ts} | 7 +- ...teSongEndpointHandler.ts => CreateSong.ts} | 7 +- ...eateTagEndpointHandler.ts => CreateTag.ts} | 7 +- ...leteTagEndpointHandler.ts => DeleteTag.ts} | 14 +- ...MergeTagEndpointHandler.ts => MergeTag.ts} | 7 +- ...AlbumEndpointHandler.ts => ModifyAlbum.ts} | 5 +- ...tistEndpointHandler.ts => ModifyArtist.ts} | 6 +- ...fySongEndpointHandler.ts => ModifySong.ts} | 5 +- ...difyTagEndpointHandler.ts => ModifyTag.ts} | 6 +- .../{QueryEndpointHandler.ts => Query.ts} | 25 +- server/endpoints/RegisterUser.ts | 49 + ...tailsEndpointHandler.ts => SongDetails.ts} | 22 +- ...etailsEndpointHandler.ts => TagDetails.ts} | 16 +- server/migrations/20201110170100_add_users.ts | 73 + server/package-lock.json | 32 + server/package.json | 3 + server/test/integration/flows/AlbumFlow.js | 132 +- server/test/integration/flows/ArtistFlow.js | 126 +- server/test/integration/flows/AuthFlow.js | 145 ++ server/test/integration/flows/QueryFlow.js | 457 +++--- server/test/integration/flows/SongFlow.js | 170 +- server/test/integration/flows/TagFlow.js | 108 +- server/test/integration/flows/helpers.js | 45 + 52 files changed, 1883 insertions(+), 2141 deletions(-) create mode 100644 client/src/components/windows/login/LoginWindow.tsx create mode 100644 client/src/components/windows/register/RegisterWindow.tsx create mode 100644 client/src/lib/useAuth.tsx rename server/endpoints/{AlbumDetailsEndpointHandler.ts => AlbumDetails.ts} (78%) rename server/endpoints/{ArtistDetailsEndpointHandler.ts => ArtistDetails.ts} (70%) rename server/endpoints/{CreateAlbumEndpointHandler.ts => CreateAlbum.ts} (93%) rename server/endpoints/{CreateArtistEndpointHandler.ts => CreateArtist.ts} (92%) rename server/endpoints/{CreateSongEndpointHandler.ts => CreateSong.ts} (93%) rename server/endpoints/{CreateTagEndpointHandler.ts => CreateTag.ts} (89%) rename server/endpoints/{DeleteTagEndpointHandler.ts => DeleteTag.ts} (83%) rename server/endpoints/{MergeTagEndpointHandler.ts => MergeTag.ts} (90%) rename server/endpoints/{ModifyAlbumEndpointHandler.ts => ModifyAlbum.ts} (96%) rename server/endpoints/{ModifyArtistEndpointHandler.ts => ModifyArtist.ts} (95%) rename server/endpoints/{ModifySongEndpointHandler.ts => ModifySong.ts} (97%) rename server/endpoints/{ModifyTagEndpointHandler.ts => ModifyTag.ts} (90%) rename server/endpoints/{QueryEndpointHandler.ts => Query.ts} (92%) create mode 100644 server/endpoints/RegisterUser.ts rename server/endpoints/{SongDetailsEndpointHandler.ts => SongDetails.ts} (80%) rename server/endpoints/{TagDetailsEndpointHandler.ts => TagDetails.ts} (65%) create mode 100644 server/migrations/20201110170100_add_users.ts create mode 100644 server/test/integration/flows/AuthFlow.js diff --git a/client/src/App.tsx b/client/src/App.tsx index 075a107..08a6107 100644 --- a/client/src/App.tsx +++ b/client/src/App.tsx @@ -4,11 +4,14 @@ import { DndProvider } from 'react-dnd'; import { HTML5Backend } from 'react-dnd-html5-backend'; import MainWindow from './components/MainWindow'; +import { ProvideAuth } from './lib/useAuth'; function App() { return ( - + + + ); } diff --git a/client/src/api.ts b/client/src/api.ts index 7811d5d..d1cb334 100644 --- a/client/src/api.ts +++ b/client/src/api.ts @@ -318,4 +318,42 @@ export interface MergeTagRequest { } export interface MergeTagResponse { } export function checkMergeTagRequest(req: any): boolean { return true; -} \ No newline at end of file +} + +// Register a user (POST). +// TODO: add e-mail verification. +export const RegisterUserEndpoint = '/register'; +export interface RegisterUserRequest { + email: string, + password: string, +} +export interface RegisterUserResponse { } +export function checkPassword(password: string): boolean { + const result = (password.length < 32) && + (password.length >= 8) && + password.split("").every(char => char.charCodeAt(0) <= 127) && // is ASCII + (/[a-z]/g.test(password)) && // has lowercase + (/[A-Z]/g.test(password)) && // has uppercase + (/[0-9]/g.test(password)) && // has number + (/[!@#$%^&*()_+/]/g.test(password)) // has special character; + + console.log("Password check for ", password, ": ", result); + return result; +} +export function checkEmail(email: string): boolean { + const re = /^[a-zA-Z0-9.!#$%&'*+/=?^_`{|}~-]+@[a-zA-Z0-9-]+(?:\.[a-zA-Z0-9-]+)*$/; + const result = re.test(String(email).toLowerCase()); + console.log("Email check for ", email, ": ", result); + return result; +} +export function checkRegisterUserRequest(req: any): boolean { + return "body" in req && + "email" in req.body && + "password" in req.body && + checkEmail(req.body.email) && + checkPassword(req.body.password); +} + +// Note: Login is handled by Passport.js, so it is not explicitly written here. +export const LoginEndpoint = "/login"; +export const LogoutEndpoint = "/logout"; \ No newline at end of file diff --git a/client/src/components/MainWindow.tsx b/client/src/components/MainWindow.tsx index 2839106..2241615 100644 --- a/client/src/components/MainWindow.tsx +++ b/client/src/components/MainWindow.tsx @@ -1,16 +1,17 @@ -import React, { useReducer, Reducer, useContext } from 'react'; +import React from 'react'; import { ThemeProvider, CssBaseline, createMuiTheme } from '@material-ui/core'; import { grey } from '@material-ui/core/colors'; import AppBar, { AppBarTab } from './appbar/AppBar'; -import QueryWindow, { QueryWindowReducer } from './windows/query/QueryWindow'; -import { newWindowState, newWindowReducer, WindowType } from './windows/Windows'; +import QueryWindow from './windows/query/QueryWindow'; import ArtistWindow from './windows/artist/ArtistWindow'; import AlbumWindow from './windows/album/AlbumWindow'; import TagWindow from './windows/tag/TagWindow'; import SongWindow from './windows/song/SongWindow'; import ManageTagsWindow from './windows/manage_tags/ManageTagsWindow'; -import { BrowserRouter, Switch, Route, useParams, Redirect } from 'react-router-dom'; -var _ = require('lodash'); +import { BrowserRouter, Switch, Route, Redirect } from 'react-router-dom'; +import LoginWindow from './windows/login/LoginWindow'; +import { useAuth } from '../lib/useAuth'; +import RegisterWindow from './windows/register/RegisterWindow'; const darkTheme = createMuiTheme({ palette: { @@ -21,6 +22,25 @@ const darkTheme = createMuiTheme({ }, }); +function PrivateRoute(props: any) { + const { children, ...rest } = props; + let auth = useAuth(); + return + auth.user ? ( + children + ) : ( + + ) + } + /> +} + export default function MainWindow(props: any) { return @@ -29,30 +49,38 @@ export default function MainWindow(props: any) { - - - - - + - + - + - + - + + + + + - - - + + + - - - + + + + + + + + + + + - - + + diff --git a/client/src/components/appbar/AppBar.tsx b/client/src/components/appbar/AppBar.tsx index 28809f2..b39601f 100644 --- a/client/src/components/appbar/AppBar.tsx +++ b/client/src/components/appbar/AppBar.tsx @@ -1,10 +1,9 @@ import React from 'react'; -import { AppBar as MuiAppBar, Box, Tab as MuiTab, Tabs, IconButton, Typography } from '@material-ui/core'; -import CloseIcon from '@material-ui/icons/Close'; +import { AppBar as MuiAppBar, Box, Tab as MuiTab, Tabs, IconButton, Typography, Menu, MenuItem } from '@material-ui/core'; import SearchIcon from '@material-ui/icons/Search'; import LocalOfferIcon from '@material-ui/icons/LocalOffer'; import { Link, useHistory } from 'react-router-dom'; -import { WindowType } from '../windows/Windows'; +import { useAuth } from '../../lib/useAuth'; export enum AppBarTab { Query = 0, @@ -13,19 +12,58 @@ export enum AppBarTab { export const appBarTabProps: Record = { [AppBarTab.Query]: { - label: Query, + label: Query, path: "/query", }, [AppBarTab.Tags]: { - label: Tags, + label: Tags, path: "/tags", }, } +export function UserMenu(props: { + position: null | number[], + open: boolean, + onLogout: () => void, + onClose: () => void, +}) { + let auth = useAuth(); + const pos = props.open && props.position ? + { left: props.position[0], top: props.position[1] } + : { left: 0, top: 0 } + + return + + {auth.user?.email || "Unknown user"} + { + props.onClose(); + props.onLogout(); + }} + >Sign out + + +} + export default function AppBar(props: { selectedTab: AppBarTab | null }) { - const history = useHistory(); + let history = useHistory(); + let auth = useAuth(); + + const [userMenuPos, setUserMenuPos] = React.useState(null); + const onOpenUserMenu = (e: any) => { + setUserMenuPos([e.clientX, e.clientY]) + }; + const onCloseUserMenu = () => { + setUserMenuPos(null); + }; return <> @@ -35,18 +73,30 @@ export default function AppBar(props: { error - history.push(appBarTabProps[val].path)} - variant="scrollable" - scrollButtons="auto" - > - {Object.keys(appBarTabProps).map((tab: any, idx: number) => )} - + + {auth.user && history.push(appBarTabProps[val].path)} + variant="scrollable" + scrollButtons="auto" + > + {Object.keys(appBarTabProps).map((tab: any, idx: number) => )} + } + + {auth.user && { onOpenUserMenu(e) }} + >{auth.user.icon}} + } \ No newline at end of file diff --git a/client/src/components/querybuilder/QBLeafElem.tsx b/client/src/components/querybuilder/QBLeafElem.tsx index 6398b7e..dad3d03 100644 --- a/client/src/components/querybuilder/QBLeafElem.tsx +++ b/client/src/components/querybuilder/QBLeafElem.tsx @@ -110,56 +110,56 @@ export function QBLeafElem(props: IProps) { : undefined; - if (e.a == QueryLeafBy.ArtistName && - e.leafOp == QueryLeafOp.Equals && + if (e.a === QueryLeafBy.ArtistName && + e.leafOp === QueryLeafOp.Equals && typeof e.b == "string") { return - } else if (e.a == QueryLeafBy.ArtistName && - e.leafOp == QueryLeafOp.Like && + } else if (e.a === QueryLeafBy.ArtistName && + e.leafOp === QueryLeafOp.Like && typeof e.b == "string") { return - } else if (e.a == QueryLeafBy.AlbumName && - e.leafOp == QueryLeafOp.Equals && + } else if (e.a === QueryLeafBy.AlbumName && + e.leafOp === QueryLeafOp.Equals && typeof e.b == "string") { return - } else if (e.a == QueryLeafBy.AlbumName && - e.leafOp == QueryLeafOp.Like && + } else if (e.a === QueryLeafBy.AlbumName && + e.leafOp === QueryLeafOp.Like && typeof e.b == "string") { return - } if (e.a == QueryLeafBy.SongTitle && - e.leafOp == QueryLeafOp.Equals && + } if (e.a === QueryLeafBy.SongTitle && + e.leafOp === QueryLeafOp.Equals && typeof e.b == "string") { return - } else if (e.a == QueryLeafBy.SongTitle && - e.leafOp == QueryLeafOp.Like && + } else if (e.a === QueryLeafBy.SongTitle && + e.leafOp === QueryLeafOp.Like && typeof e.b == "string") { return - } else if (e.a == QueryLeafBy.TagInfo && - e.leafOp == QueryLeafOp.Equals && + } else if (e.a === QueryLeafBy.TagInfo && + e.leafOp === QueryLeafOp.Equals && isTagQueryInfo(e.b)) { return - }else if (e.leafOp == QueryLeafOp.Placeholder) { + }else if (e.leafOp === QueryLeafOp.Placeholder) { return }); - if (e.nodeOp == QueryNodeOp.And) { + if (e.nodeOp === QueryNodeOp.And) { return {children} - } else if (e.nodeOp == QueryNodeOp.Or) { + } else if (e.nodeOp === QueryNodeOp.Or) { return {children} } - throw "Unsupported node element"; + throw new Error("Unsupported node element"); } \ No newline at end of file diff --git a/client/src/components/querybuilder/QBSelectWithRequest.tsx b/client/src/components/querybuilder/QBSelectWithRequest.tsx index 107e982..77d4574 100644 --- a/client/src/components/querybuilder/QBSelectWithRequest.tsx +++ b/client/src/components/querybuilder/QBSelectWithRequest.tsx @@ -1,4 +1,4 @@ -import React, { useState, useEffect } from 'react'; +import React, { useState, useEffect, useCallback } from 'react'; import TextField from '@material-ui/core/TextField'; import Autocomplete from '@material-ui/lab/Autocomplete'; import CircularProgress from '@material-ui/core/CircularProgress'; @@ -26,26 +26,26 @@ export default function QBSelectWithRequest(props: IProps & any) { const loading: boolean = !options || options.forInput !== input; - const updateOptions = (forInput: string, options: any[]) => { + const updateOptions = useCallback((forInput: string, options: any[]) => { if (forInput === input) { setOptions({ forInput: forInput, options: options, }); } - } + }, [setOptions, input]); - const startRequest = (_input: string) => { + const startRequest = useCallback((_input: string) => { setInput(_input); (async () => { const newOptions = await getNewOptions(_input); updateOptions(_input, newOptions); })(); - }; + }, [setInput, getNewOptions, updateOptions]); useEffect(() => { startRequest(input); - }, [input]); + }, [input, startRequest]); const onInputChange = (e: any, val: any, reason: any) => { if (reason === 'reset') { diff --git a/client/src/components/tables/ResultsTable.tsx b/client/src/components/tables/ResultsTable.tsx index 5fee3d1..e79fb37 100644 --- a/client/src/components/tables/ResultsTable.tsx +++ b/client/src/components/tables/ResultsTable.tsx @@ -46,14 +46,12 @@ export default function SongTable(props: { {props.songs.map((song: any) => { const title = props.songGetters.getTitle(song); - // TODO / FIXME: display artists and albums separately! + // TODO: display artists and albums separately! const artistNames = props.songGetters.getArtistNames(song); const artist = stringifyList(artistNames); const mainArtistId = props.songGetters.getArtistIds(song)[0]; - const mainArtistName = artistNames[0]; const albumNames = props.songGetters.getAlbumNames(song); const album = stringifyList(albumNames); - const mainAlbumName = albumNames[0]; const mainAlbumId = props.songGetters.getAlbumIds(song)[0]; const songId = props.songGetters.getId(song); const tagIds = props.songGetters.getTagIds(song); diff --git a/client/src/components/windows/Windows.tsx b/client/src/components/windows/Windows.tsx index edd6768..9188b2f 100644 --- a/client/src/components/windows/Windows.tsx +++ b/client/src/components/windows/Windows.tsx @@ -12,6 +12,8 @@ import AlbumWindow, { AlbumWindowReducer } from './album/AlbumWindow'; import TagWindow, { TagWindowReducer } from './tag/TagWindow'; import { songGetters } from '../../lib/songGetters'; import ManageTagsWindow, { ManageTagsWindowReducer } from './manage_tags/ManageTagsWindow'; +import { RegisterWindowReducer } from './register/RegisterWindow'; +import { LoginWindowReducer } from './login/LoginWindow'; export enum WindowType { Query = "Query", @@ -20,6 +22,8 @@ export enum WindowType { Tag = "Tag", Song = "Song", ManageTags = "ManageTags", + Login = "Login", + Register = "Register", } export interface WindowState { } @@ -31,6 +35,8 @@ export const newWindowReducer = { [WindowType.Song]: SongWindowReducer, [WindowType.Tag]: TagWindowReducer, [WindowType.ManageTags]: ManageTagsWindowReducer, + [WindowType.Login]: LoginWindowReducer, + [WindowType.Register]: RegisterWindowReducer, } export const newWindowState = { @@ -81,5 +87,11 @@ export const newWindowState = { alert: null, pendingChanges: [], } - } + }, + [WindowType.Login]: () => { + return {} + }, + [WindowType.Register]: () => { + return {} + }, } \ No newline at end of file diff --git a/client/src/components/windows/album/AlbumWindow.tsx b/client/src/components/windows/album/AlbumWindow.tsx index 07daa38..7d14329 100644 --- a/client/src/components/windows/album/AlbumWindow.tsx +++ b/client/src/components/windows/album/AlbumWindow.tsx @@ -12,7 +12,6 @@ import { QueryLeafBy, QueryLeafOp } from '../../../lib/query/Query'; import { queryAlbums, querySongs } from '../../../lib/backend/queries'; import { songGetters } from '../../../lib/songGetters'; import { useParams } from 'react-router'; -var _ = require('lodash'); export type AlbumMetadata = serverApi.AlbumDetails; export type AlbumMetadataChanges = serverApi.ModifyAlbumRequest; @@ -76,40 +75,40 @@ export function AlbumWindowControlled(props: { state: AlbumWindowState, dispatch: (action: any) => void, }) { - let metadata = props.state.metadata; - let pendingChanges = props.state.pendingChanges; + let { id: albumId, metadata, pendingChanges, songsOnAlbum } = props.state; + let { dispatch } = props; // Effect to get the album's metadata. useEffect(() => { - getAlbumMetadata(props.state.id) + getAlbumMetadata(albumId) .then((m: AlbumMetadata) => { - props.dispatch({ + dispatch({ type: AlbumWindowStateActions.SetMetadata, value: m }); }) - }, [metadata?.name]); + }, [albumId, dispatch]); // Effect to get the album's songs. useEffect(() => { - if (props.state.songsOnAlbum) { return; } + if (songsOnAlbum) { return; } (async () => { const songs = await querySongs({ query: { a: QueryLeafBy.AlbumId, - b: props.state.id, + b: albumId, leafOp: QueryLeafOp.Equals, }, offset: 0, limit: -1, }); - props.dispatch({ + dispatch({ type: AlbumWindowStateActions.SetSongs, value: songs, }); })(); - }, [props.state.songsOnAlbum]); + }, [songsOnAlbum, albumId, dispatch]); const [editingName, setEditingName] = useState(null); const name = { const store = whichStore(link); return store && void, }) { - let metadata = props.state.metadata; - let pendingChanges = props.state.pendingChanges; + let { metadata, id: artistId, pendingChanges, songsByArtist } = props.state; + let { dispatch } = props; // Effect to get the artist's metadata. useEffect(() => { - getArtistMetadata(props.state.id) + getArtistMetadata(artistId) .then((m: ArtistMetadata) => { - props.dispatch({ + dispatch({ type: ArtistWindowStateActions.SetMetadata, value: m }); }) - }, [metadata?.name]); + }, [artistId, dispatch]); // Effect to get the artist's songs. useEffect(() => { - if (props.state.songsByArtist) { return; } + if (songsByArtist) { return; } (async () => { const songs = await querySongs({ query: { a: QueryLeafBy.ArtistId, - b: props.state.id, + b: artistId, leafOp: QueryLeafOp.Equals, }, offset: 0, limit: -1, }); - props.dispatch({ + dispatch({ type: ArtistWindowStateActions.SetSongs, value: songs, }); })(); - }, [props.state.songsByArtist]); + }, [songsByArtist, dispatch, artistId]); const [editingName, setEditingName] = useState(null); const name = { const store = whichStore(link); return store && +} + +export function LoginWindowControlled(props: { + state: LoginWindowState, + dispatch: (action: any) => void, +}) { + let history: any = useHistory(); + let location: any = useLocation(); + let auth: Auth = useAuth(); + let { from } = location.state || { from: { pathname: "/" } }; + + const onSubmit = (event: any) => { + event.preventDefault(); + auth.signin(props.state.email, props.state.password) + .then(() => { + history.replace(from); + }).catch((e: any) => { + props.dispatch({ + type: LoginWindowStateActions.SetStatus, + value: LoginStatus.Unsuccessful, + }) + }) + } + + return + + + + Sign in +
+ props.dispatch({ + type: LoginWindowStateActions.SetEmail, + value: e.target.value + })} + /> + props.dispatch({ + type: LoginWindowStateActions.SetPassword, + value: e.target.value + })} + /> + {props.state.status === LoginStatus.Unsuccessful && + Login failed - Please check your credentials. + + } + + + Need an account? + + + +
+
+
+
+} \ No newline at end of file diff --git a/client/src/components/windows/manage_tags/ManageTagMenu.tsx b/client/src/components/windows/manage_tags/ManageTagMenu.tsx index 376f340..38284ac 100644 --- a/client/src/components/windows/manage_tags/ManageTagMenu.tsx +++ b/client/src/components/windows/manage_tags/ManageTagMenu.tsx @@ -1,5 +1,5 @@ -import React, { useState } from 'react'; -import { Menu, MenuItem, TextField, Input } from '@material-ui/core'; +import React from 'react'; +import { Menu, MenuItem } from '@material-ui/core'; import NestedMenuItem from "material-ui-nested-menu-item"; import MenuEditText from '../../common/MenuEditText'; diff --git a/client/src/components/windows/manage_tags/ManageTagsWindow.tsx b/client/src/components/windows/manage_tags/ManageTagsWindow.tsx index 285dfdd..9e4dd7e 100644 --- a/client/src/components/windows/manage_tags/ManageTagsWindow.tsx +++ b/client/src/components/windows/manage_tags/ManageTagsWindow.tsx @@ -1,5 +1,5 @@ import React, { useEffect, useState, ReactFragment, useReducer } from 'react'; -import { WindowState, newWindowReducer, WindowType } from '../Windows'; +import { WindowState } from '../Windows'; import { Box, Typography, Chip, IconButton, useTheme, Button } from '@material-ui/core'; import LoyaltyIcon from '@material-ui/icons/Loyalty'; import ArrowRightIcon from '@material-ui/icons/ArrowRight'; @@ -9,8 +9,6 @@ import ControlTagChanges, { TagChange, TagChangeType, submitTagChanges } from '. import { queryTags } from '../../../lib/backend/queries'; import NewTagMenu from './NewTagMenu'; import { v4 as genUuid } from 'uuid'; -import LocalOfferIcon from '@material-ui/icons/LocalOffer'; -import { songGetters } from '../../../lib/songGetters'; import Alert from '@material-ui/lab/Alert'; import { useHistory } from 'react-router'; var _ = require('lodash'); @@ -355,6 +353,8 @@ export function ManageTagsWindowControlled(props: { dispatch: (action: any) => void, }) { const [newTagMenuPos, setNewTagMenuPos] = React.useState(null); + let { fetchedTags } = props.state; + let { dispatch } = props; const onOpenNewTagMenu = (e: any) => { setNewTagMenuPos([e.clientX, e.clientY]) @@ -364,19 +364,19 @@ export function ManageTagsWindowControlled(props: { }; useEffect(() => { - if (props.state.fetchedTags !== null) { + if (fetchedTags !== null) { return; } (async () => { const allTags = await getAllTags(); // We have the tags in list form. Now, we want to organize // them hierarchically by giving each tag a "children" prop. - props.dispatch({ + dispatch({ type: ManageTagsWindowActions.SetFetchedTags, value: allTags, }); })(); - }, [props.state.fetchedTags]); + }, [fetchedTags, dispatch]); const tagsWithChanges = annotateTagsWithChanges(props.state.fetchedTags || {}, props.state.pendingChanges || []) const changedTags = organiseTags( diff --git a/client/src/components/windows/manage_tags/NewTagMenu.tsx b/client/src/components/windows/manage_tags/NewTagMenu.tsx index 3d16315..988ed93 100644 --- a/client/src/components/windows/manage_tags/NewTagMenu.tsx +++ b/client/src/components/windows/manage_tags/NewTagMenu.tsx @@ -1,5 +1,5 @@ -import React, { useState } from 'react'; -import { Menu, MenuItem, TextField, Input } from '@material-ui/core'; +import React from 'react'; +import { Menu } from '@material-ui/core'; import NestedMenuItem from "material-ui-nested-menu-item"; import MenuEditText from '../../common/MenuEditText'; diff --git a/client/src/components/windows/manage_tags/TagChange.tsx b/client/src/components/windows/manage_tags/TagChange.tsx index cce1c06..b2e9c35 100644 --- a/client/src/components/windows/manage_tags/TagChange.tsx +++ b/client/src/components/windows/manage_tags/TagChange.tsx @@ -1,7 +1,5 @@ -import React, { useState, useEffect } from 'react'; +import React from 'react'; import { Typography, Chip, CircularProgress, Box, Paper } from '@material-ui/core'; -import { queryTags } from '../../../lib/backend/queries'; -import { QueryLeafBy, QueryLeafOp } from '../../../lib/query/Query'; import DiscardChangesButton from '../../common/DiscardChangesButton'; import SubmitChangesButton from '../../common/SubmitChangesButton'; import { createTag, modifyTag, deleteTag, mergeTag } from '../../../lib/backend/tags'; @@ -31,7 +29,7 @@ export async function submitTagChanges(changes: TagChange[]) { var id_lookup: Record = {} const getId = (id_string: string) => { - return (Number(id_string) === NaN) ? + return (isNaN(Number(id_string))) ? id_lookup[id_string] : Number(id_string); } diff --git a/client/src/components/windows/query/QueryWindow.tsx b/client/src/components/windows/query/QueryWindow.tsx index 9561883..85afec5 100644 --- a/client/src/components/windows/query/QueryWindow.tsx +++ b/client/src/components/windows/query/QueryWindow.tsx @@ -1,24 +1,13 @@ -import React, { useEffect, useReducer } from 'react'; -import { createMuiTheme, Box, LinearProgress } from '@material-ui/core'; -import { QueryElem, toApiQuery, QueryLeafBy, QueryLeafOp } from '../../../lib/query/Query'; +import React, { useEffect, useReducer, useCallback } from 'react'; +import { Box, LinearProgress } from '@material-ui/core'; +import { QueryElem, QueryLeafBy, QueryLeafOp } from '../../../lib/query/Query'; import QueryBuilder from '../../querybuilder/QueryBuilder'; -import * as serverApi from '../../../api'; import SongTable from '../../tables/ResultsTable'; import { songGetters } from '../../../lib/songGetters'; import { queryArtists, querySongs, queryAlbums, queryTags } from '../../../lib/backend/queries'; -import { grey } from '@material-ui/core/colors'; import { WindowState } from '../Windows'; var _ = require('lodash'); -const darkTheme = createMuiTheme({ - palette: { - type: 'dark', - primary: { - main: grey[100], - } - }, -}); - export interface ResultsForQuery { for: QueryElem, results: any[], @@ -112,23 +101,23 @@ export function QueryWindowControlled(props: { state: QueryWindowState, dispatch: (action: any) => void, }) { - let query = props.state.query; - let editing = props.state.editingQuery; - let resultsFor = props.state.resultsForQuery; + let { query, editingQuery: editing, resultsForQuery: resultsFor } = props.state; + let { dispatch } = props; + let setQuery = (q: QueryElem | null) => { props.dispatch({ type: QueryWindowStateActions.SetQuery, value: q }); } let setEditingQuery = (e: boolean) => { props.dispatch({ type: QueryWindowStateActions.SetEditingQuery, value: e }); } - let setResultsForQuery = (r: ResultsForQuery | null) => { - props.dispatch({ type: QueryWindowStateActions.SetResultsForQuery, value: r }); - } + let setResultsForQuery = useCallback((r: ResultsForQuery | null) => { + dispatch({ type: QueryWindowStateActions.SetResultsForQuery, value: r }); + }, [ dispatch ]); const loading = query && (!resultsFor || !_.isEqual(resultsFor.for, query)); - const showResults = (query && resultsFor && query == resultsFor.for) ? resultsFor.results : []; + const showResults = (query && resultsFor && query === resultsFor.for) ? resultsFor.results : []; - const doQuery = async (_query: QueryElem) => { + const doQuery = useCallback(async (_query: QueryElem) => { const songs = await querySongs({ query: _query, offset: 0, @@ -141,7 +130,7 @@ export function QueryWindowControlled(props: { results: songs, }) } - } + }, [query, setResultsForQuery]); useEffect(() => { if (query) { @@ -149,7 +138,7 @@ export function QueryWindowControlled(props: { } else { setResultsForQuery(null); } - }, [query]); + }, [query, doQuery, setResultsForQuery]); return +} + +export function RegisterWindowControlled(props: { + state: RegisterWindowState, + dispatch: (action: any) => void, +}) { + let history: any = useHistory(); + let auth: Auth = useAuth(); + + const onSubmit = (event: any) => { + event.preventDefault(); + auth.signup(props.state.email, props.state.password) + .then(() => { + console.log("succes!") + props.dispatch({ + type: RegisterWindowStateActions.SetStatus, + value: RegistrationStatus.Successful, + }) + }).catch((e: any) => { + console.log("Fail!") + props.dispatch({ + type: RegisterWindowStateActions.SetStatus, + value: RegistrationStatus.Unsuccessful, + }) + }) + } + + return + + + + Sign up +
+ props.dispatch({ + type: RegisterWindowStateActions.SetEmail, + value: e.target.value + })} + /> + props.dispatch({ + type: RegisterWindowStateActions.SetPassword, + value: e.target.value + })} + /> + {props.state.status === RegistrationStatus.Successful && + Registration successful! Please {sign in} to continue. + + } + {props.state.status === RegistrationStatus.Unsuccessful && + Registration failed - please check your inputs and try again. + + } + {props.state.status !== RegistrationStatus.Successful && } + + Already have an account? + + + +
+
+
+
+} \ No newline at end of file diff --git a/client/src/components/windows/song/SongWindow.tsx b/client/src/components/windows/song/SongWindow.tsx index 6016366..c0f9e93 100644 --- a/client/src/components/windows/song/SongWindow.tsx +++ b/client/src/components/windows/song/SongWindow.tsx @@ -1,5 +1,5 @@ import React, { useEffect, useState, useReducer } from 'react'; -import { Box, Typography, IconButton, Button, CircularProgress } from '@material-ui/core'; +import { Box, Typography, IconButton, CircularProgress } from '@material-ui/core'; import AudiotrackIcon from '@material-ui/icons/Audiotrack'; import PersonIcon from '@material-ui/icons/Person'; import AlbumIcon from '@material-ui/icons/Album'; @@ -13,7 +13,6 @@ import SubmitChangesButton from '../../common/SubmitChangesButton'; import { saveSongChanges } from '../../../lib/saveChanges'; import { QueryLeafBy, QueryLeafOp } from '../../../lib/query/Query'; import { querySongs } from '../../../lib/backend/queries'; -import { songGetters } from '../../../lib/songGetters'; import { useParams } from 'react-router'; export type SongMetadata = serverApi.SongDetails; @@ -44,11 +43,6 @@ export function SongWindowReducer(state: SongWindowState, action: any) { } } -export interface IProps { - state: SongWindowState, - dispatch: (action: any) => void, -} - export async function getSongMetadata(id: number) { return (await querySongs({ query: { @@ -76,18 +70,18 @@ export function SongWindowControlled(props: { state: SongWindowState, dispatch: (action: any) => void, }) { - let metadata = props.state.metadata; - let pendingChanges = props.state.pendingChanges; + let { pendingChanges, metadata, id: songId } = props.state; + let { dispatch } = props; useEffect(() => { - getSongMetadata(props.state.id) + getSongMetadata(songId) .then((m: SongMetadata) => { - props.dispatch({ + dispatch({ type: SongWindowStateActions.SetMetadata, value: m }); }) - }, [metadata?.title]); + }, [songId, dispatch]); const [editingTitle, setEditingTitle] = useState(null); const title = { const store = whichStore(link); return store &&
{ - getTagMetadata(props.state.id) + getTagMetadata(tagId) .then((m: TagMetadata) => { - props.dispatch({ + dispatch({ type: TagWindowStateActions.SetMetadata, value: m }); }) - }, [metadata?.name]); + }, [tagId, dispatch]); // Effect to get the tag's songs. useEffect(() => { - if (props.state.songsWithTag) { return; } + if (songsWithTag) { return; } (async () => { const songs = await querySongs({ query: { a: QueryLeafBy.TagId, - b: props.state.id, + b: tagId, leafOp: QueryLeafOp.Equals, }, offset: 0, limit: -1, }); - props.dispatch({ + dispatch({ type: TagWindowStateActions.SetSongs, value: songs, - }); + }); })(); - }, [props.state.songsWithTag]); + }, [songsWithTag, tagId, dispatch]); const [editingName, setEditingName] = useState(null); const name = const fullName = {metadata?.fullName.map((n: string, i: number) => { - if (metadata?.fullName && i == metadata?.fullName.length - 1) { + if (metadata?.fullName && i === metadata?.fullName.length - 1) { return name; } else if (i >= (metadata?.fullName.length || 0) - 1) { return undefined; @@ -160,7 +161,7 @@ export function TagWindowControlled(props: { const storeLinks = metadata?.storeLinks && metadata?.storeLinks.map((link: string) => { const store = whichStore(link); return store && { - if (isLeafElem(op) && op.leafOp == QueryLeafOp.Placeholder) { + if (isLeafElem(op) && op.leafOp === QueryLeafOp.Placeholder) { return; } const newOp = removePlaceholders(op); @@ -136,14 +136,14 @@ export function removePlaceholders(q: QueryElem | null): QueryElem | null { } }) - if (newOperands.length == 0) { + if (newOperands.length === 0) { return null; } - if (newOperands.length == 1) { + if (newOperands.length === 1) { return newOperands[0]; } return { operands: newOperands, nodeOp: q.nodeOp }; - } else if (q && isLeafElem(q) && q.leafOp == QueryLeafOp.Placeholder) { + } else if (q && isLeafElem(q) && q.leafOp === QueryLeafOp.Placeholder) { return null; } diff --git a/client/src/lib/useAuth.tsx b/client/src/lib/useAuth.tsx new file mode 100644 index 0000000..d6b4995 --- /dev/null +++ b/client/src/lib/useAuth.tsx @@ -0,0 +1,102 @@ +// Note: Based on https://usehooks.com/useAuth/ + + +import React, { useState, useContext, createContext, ReactFragment } from "react"; +import PersonIcon from '@material-ui/icons/Person'; +import * as serverApi from '../api'; + +export interface AuthUser { + id: number, + email: string, + icon: ReactFragment, +} + +export interface Auth { + user: AuthUser | null, + signout: () => void, + signin: (email: string, password: string) => Promise, + signup: (email: string, password: string) => Promise, +}; + +const authContext = createContext({ + user: null, + signout: () => { }, + signin: (email: string, password: string) => { + throw new Error("Auth object not initialized."); + }, + signup: (email: string, password: string) => { + throw new Error("Auth object not initialized."); + }, +}); + +export function ProvideAuth(props: { children: any }) { + const auth = useProvideAuth(); + return {props.children}; +} + +export const useAuth = () => { + return useContext(authContext); +}; + +function useProvideAuth() { + const [user, setUser] = useState(null); + + // TODO: password maybe shouldn't be encoded into the URL. + const signin = (email: string, password: string) => { + return (async () => { + const urlBase = (process.env.REACT_APP_BACKEND || "") + serverApi.LoginEndpoint; + const url = `${urlBase}?username=${encodeURIComponent(email)}&password=${encodeURIComponent(password)}`; + + const response = await fetch(url, { method: "POST" }); + const json = await response.json(); + if (!("userId" in json)) { + throw new Error("No UserID received from login."); + } + + const user = { + id: json.userId, + email: email, + icon: , + } + setUser(user); + return user; + })(); + }; + + const signup = (email: string, password: string) => { + return (async () => { + const requestOpts = { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + email: email, + password: password, + }) + }; + + const response = await fetch((process.env.REACT_APP_BACKEND || "") + serverApi.RegisterUserEndpoint, requestOpts) + if (!response.ok) { + throw new Error("Failed to register user.") + } + })(); + }; + + const signout = () => { + return (async () => { + const url = (process.env.REACT_APP_BACKEND || "") + serverApi.LogoutEndpoint; + const response = await fetch(url, { method: "POST" }); + if (!response.ok) { + throw new Error("Failed to log out."); + } + setUser(null); + })(); + }; + + // Return the user object and auth methods + return { + user, + signin, + signup, + signout, + }; +} \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index 2eccf81..f7b29a3 100644 --- a/package-lock.json +++ b/package-lock.json @@ -4,37 +4,6 @@ "lockfileVersion": 1, "requires": true, "dependencies": { - "@types/node": { - "version": "14.0.19", - "resolved": "https://registry.npmjs.org/@types/node/-/node-14.0.19.tgz", - "integrity": "sha512-yf3BP/NIXF37BjrK5klu//asUWitOEoUP5xE1mhSUjazotwJ/eJDgEmMQNlOeWOVv72j24QQ+3bqXHE++CFGag==" - }, - "abbrev": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz", - "integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==" - }, - "accepts": { - "version": "1.3.7", - "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.7.tgz", - "integrity": "sha512-Il80Qs2WjYlJIBNzNkK6KYqlVMTbZLXgHx2oT0pU/fjRHyEp+PEfEPY0R3WCwAGVOtauxh1hOxNgIf5bv7dQpA==", - "requires": { - "mime-types": "~2.1.24", - "negotiator": "0.6.2" - } - }, - "ajv": { - "version": "6.12.3", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.3.tgz", - "integrity": "sha512-4K0cK3L1hsqk9xIb2z9vs/XU+PGJZ9PNpJRDS9YLzmNdX6jmVPfamLvTJr0aDAusnHyCHO6MjzlkAsgtqp9teA==", - "optional": true, - "requires": { - "fast-deep-equal": "^3.1.1", - "fast-json-stable-stringify": "^2.0.0", - "json-schema-traverse": "^0.4.1", - "uri-js": "^4.2.2" - } - }, "ansi-regex": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", @@ -44,122 +13,19 @@ "version": "3.2.1", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", - "dev": true, "requires": { "color-convert": "^1.9.0" } }, - "any-promise": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/any-promise/-/any-promise-1.3.0.tgz", - "integrity": "sha1-q8av7tzqUugJzcA3au0845Y10X8=" - }, - "aproba": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/aproba/-/aproba-1.2.0.tgz", - "integrity": "sha512-Y9J6ZjXtoYh8RnXVCMOU/ttDmk1aBjunq9vO0ta5x85WDQiQfUF9sIPBITdbiiIVcBo03Hi3jMxigBtsddlXRw==" - }, - "are-we-there-yet": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/are-we-there-yet/-/are-we-there-yet-1.1.5.tgz", - "integrity": "sha512-5hYdAkZlcG8tOLujVDTgCT+uPX0VnpAH28gWsLfzpXYm7wP6mp5Q/gYyR7YQ0cKVJcXJnl3j2kpBan13PtQf6w==", - "requires": { - "delegates": "^1.0.0", - "readable-stream": "^2.0.6" - } - }, - "array-flatten": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", - "integrity": "sha1-ml9pkFGx5wczKPKgCJaLZOopVdI=" - }, - "asn1": { - "version": "0.2.4", - "resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.4.tgz", - "integrity": "sha512-jxwzQpLQjSmWXgwaCZE9Nz+glAG01yF1QnWgbhGwHI5A6FRIEY6IVqtHhIepHqI7/kyEyQEagBC5mBEFlIYvdg==", - "optional": true, - "requires": { - "safer-buffer": "~2.1.0" - } - }, - "assert-plus": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", - "integrity": "sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU=", - "optional": true - }, - "asynckit": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", - "integrity": "sha1-x57Zf380y48robyXkLzDZkdLS3k=", - "optional": true - }, - "aws-sign2": { - "version": "0.7.0", - "resolved": "https://registry.npmjs.org/aws-sign2/-/aws-sign2-0.7.0.tgz", - "integrity": "sha1-tG6JCTSpWR8tL2+G1+ap8bP+dqg=", - "optional": true - }, - "aws4": { - "version": "1.10.0", - "resolved": "https://registry.npmjs.org/aws4/-/aws4-1.10.0.tgz", - "integrity": "sha512-3YDiu347mtVtjpyV3u5kVqQLP242c06zwDOgpeRnybmXlYYsLbtTrUBUm8i8srONt+FWobl5aibnU1030PeeuA==", - "optional": true - }, - "balanced-match": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz", - "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=" - }, - "bcrypt-pbkdf": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz", - "integrity": "sha1-pDAdOJtqQ/m2f/PKEaP2Y342Dp4=", - "optional": true, - "requires": { - "tweetnacl": "^0.14.3" - } - }, - "block-stream": { - "version": "0.0.9", - "resolved": "https://registry.npmjs.org/block-stream/-/block-stream-0.0.9.tgz", - "integrity": "sha1-E+v+d4oDIFz+A3UUgeu0szAMEmo=", - "optional": true, - "requires": { - "inherits": "~2.0.0" - } - }, - "brace-expansion": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", - "requires": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, - "bytes": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.0.tgz", - "integrity": "sha512-zauLjrfCG+xvoyaqLoV8bLVXXNGC4JqlxFCutSDWA6fJrTo2ZuvLYTqZ7aHBLZSMOopbzwv8f+wZcVzfVTI2Dg==" - }, "camelcase": { "version": "5.3.1", "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", - "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", - "dev": true - }, - "caseless": { - "version": "0.12.0", - "resolved": "https://registry.npmjs.org/caseless/-/caseless-0.12.0.tgz", - "integrity": "sha1-G2gcIf+EAzyCZUMJBolCDRhxUdw=", - "optional": true + "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==" }, "chalk": { "version": "2.4.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", - "dev": true, "requires": { "ansi-styles": "^3.2.1", "escape-string-regexp": "^1.0.5", @@ -170,23 +36,16 @@ "version": "5.5.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", - "dev": true, "requires": { "has-flag": "^3.0.0" } } } }, - "chownr": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz", - "integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==" - }, "cliui": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/cliui/-/cliui-4.1.0.tgz", "integrity": "sha512-4FG+RSG9DL7uEwRUZXZn3SS34DiDPfzP0VOiEwtUWlE+AR2EIg+hSyvrIgUUfhdgR/UkAeW2QHgeP+hWrXs7jQ==", - "dev": true, "requires": { "string-width": "^2.1.1", "strip-ansi": "^4.0.0", @@ -196,14 +55,12 @@ "ansi-regex": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-3.0.0.tgz", - "integrity": "sha1-7QMXwyIGT3lGbAKWa922Bas32Zg=", - "dev": true + "integrity": "sha1-7QMXwyIGT3lGbAKWa922Bas32Zg=" }, "strip-ansi": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-4.0.0.tgz", "integrity": "sha1-qEeQIusaw2iocTibY1JixQXuNo8=", - "dev": true, "requires": { "ansi-regex": "^3.0.0" } @@ -219,7 +76,6 @@ "version": "1.9.3", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", - "dev": true, "requires": { "color-name": "1.1.3" } @@ -227,26 +83,12 @@ "color-name": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", - "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=", - "dev": true - }, - "combined-stream": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", - "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", - "optional": true, - "requires": { - "delayed-stream": "~1.0.0" - } - }, - "concat-map": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", - "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=" + "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=" }, "concurrently": { "version": "4.1.2", - "dev": true, + "resolved": "https://registry.npmjs.org/concurrently/-/concurrently-4.1.2.tgz", + "integrity": "sha512-Kim9SFrNr2jd8/0yNYqDTFALzUX1tvimmwFWxmp/D4mRI+kbqIIwE2RkBDrxS2ic25O1UgQMI5AtBqdtX3ynYg==", "requires": { "chalk": "^2.4.2", "date-fns": "^1.30.1", @@ -259,24 +101,6 @@ "yargs": "^12.0.5" } }, - "console-control-strings": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/console-control-strings/-/console-control-strings-1.1.0.tgz", - "integrity": "sha1-PXz0Rk22RG6mRL9LOVB/mFEAjo4=" - }, - "content-disposition": { - "version": "0.5.3", - "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.3.tgz", - "integrity": "sha512-ExO0774ikEObIAEV9kDo50o+79VCUdEB6n6lzKgGwupcVeRlhrj3qGAfwq8G6uBJjkqLrhT0qEYFcWng8z1z0g==", - "requires": { - "safe-buffer": "5.1.2" - } - }, - "content-type": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.4.tgz", - "integrity": "sha512-hIP3EEPs8tB9AT1L+NUqtwOAps4mk2Zob89MWXMHjHWg9milF/j4osnnQLXBCBFBk/tvIG/tUc9mOUJiPBhPXA==" - }, "cookie": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.4.0.tgz", @@ -287,16 +111,10 @@ "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", "integrity": "sha1-4wOogrNCzD7oylE6eZmXNNqzriw=" }, - "core-util-is": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", - "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=" - }, "cross-spawn": { "version": "6.0.5", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-6.0.5.tgz", "integrity": "sha512-eTVLrBSt7fjbDygz805pMnstIs2VTBNkRm0qxZd+M7A5XDdxVRWO5MxGBXZhjY4cqLYLdtrGqRf8mBPmzwSpWQ==", - "dev": true, "requires": { "nice-try": "^1.0.4", "path-key": "^2.0.1", @@ -305,20 +123,10 @@ "which": "^1.2.9" } }, - "dashdash": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/dashdash/-/dashdash-1.14.1.tgz", - "integrity": "sha1-hTz6D3y+L+1d4gMmuN1YEDX24vA=", - "optional": true, - "requires": { - "assert-plus": "^1.0.0" - } - }, "date-fns": { "version": "1.30.1", "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-1.30.1.tgz", - "integrity": "sha512-hBSVCvSmWC+QypYObzwGOd9wqdDpOt+0wl0KbU+R+uuZBS1jN8VsD1ss3irQDknRj5NvxiTF6oj/nDRnN/UQNw==", - "dev": true + "integrity": "sha512-hBSVCvSmWC+QypYObzwGOd9wqdDpOt+0wl0KbU+R+uuZBS1jN8VsD1ss3irQDknRj5NvxiTF6oj/nDRnN/UQNw==" }, "debug": { "version": "2.6.9", @@ -331,70 +139,12 @@ "decamelize": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz", - "integrity": "sha1-9lNNFRSCabIDUue+4m9QH5oZEpA=", - "dev": true - }, - "deep-extend": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz", - "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==" - }, - "delayed-stream": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", - "integrity": "sha1-3zrhmayt+31ECqrgsp4icrJOxhk=", - "optional": true - }, - "delegates": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz", - "integrity": "sha1-hMbhWbgZBP3KWaDvRM2HDTElD5o=" - }, - "depd": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz", - "integrity": "sha1-m81S4UwJd2PnSbJ0xDRu0uVgtak=" - }, - "destroy": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.0.4.tgz", - "integrity": "sha1-l4hXRCxEdJ5CBmE+N5RiBYJqvYA=" - }, - "detect-libc": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-1.0.3.tgz", - "integrity": "sha1-+hN8S9aY7fVc1c0CrFWfkaTEups=" - }, - "dottie": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/dottie/-/dottie-2.0.2.tgz", - "integrity": "sha512-fmrwR04lsniq/uSr8yikThDTrM7epXHBAAjH9TbeH3rEA8tdCO7mRzB9hdmdGyJCxF8KERo9CITcm3kGuoyMhg==" - }, - "ecc-jsbn": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/ecc-jsbn/-/ecc-jsbn-0.1.2.tgz", - "integrity": "sha1-OoOpBOVDUyh4dMVkt1SThoSamMk=", - "optional": true, - "requires": { - "jsbn": "~0.1.0", - "safer-buffer": "^2.1.0" - } - }, - "ee-first": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", - "integrity": "sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0=" - }, - "encodeurl": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", - "integrity": "sha1-rT/0yG7C0CkyL1oCw6mmBslbP1k=" + "integrity": "sha1-9lNNFRSCabIDUue+4m9QH5oZEpA=" }, "end-of-stream": { "version": "1.4.4", "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz", "integrity": "sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==", - "dev": true, "requires": { "once": "^1.4.0" } @@ -403,32 +153,19 @@ "version": "1.3.2", "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz", "integrity": "sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==", - "dev": true, "requires": { "is-arrayish": "^0.2.1" } }, - "escape-html": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", - "integrity": "sha1-Aljq5NPQwJdN4cFpGI7wBR0dGYg=" - }, "escape-string-regexp": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", - "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=", - "dev": true - }, - "etag": { - "version": "1.8.1", - "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", - "integrity": "sha1-Qa4u62XvpiJorr/qg6x9eSmbCIc=" + "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=" }, "execa": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/execa/-/execa-1.0.0.tgz", "integrity": "sha512-adbxcyWV46qiHyvSp50TKt05tB4tK3HcmF7/nxfAdhnox83seTDbwnaqKO4sXRy7roHAIFqJP/Rw/AuEbX61LA==", - "dev": true, "requires": { "cross-spawn": "^6.0.0", "get-stream": "^4.0.0", @@ -439,349 +176,73 @@ "strip-eof": "^1.0.0" } }, - "express": { - "version": "4.17.1", + "express-session": { + "version": "1.17.1", + "resolved": "https://registry.npmjs.org/express-session/-/express-session-1.17.1.tgz", + "integrity": "sha512-UbHwgqjxQZJiWRTMyhvWGvjBQduGCSBDhhZXYenziMFjxst5rMV+aJZ6hKPHZnPyHGsrqRICxtX8jtEbm/z36Q==", "requires": { - "accepts": "~1.3.7", - "array-flatten": "1.1.1", - "body-parser": "1.19.0", - "content-disposition": "0.5.3", - "content-type": "~1.0.4", "cookie": "0.4.0", "cookie-signature": "1.0.6", "debug": "2.6.9", - "depd": "~1.1.2", - "encodeurl": "~1.0.2", - "escape-html": "~1.0.3", - "etag": "~1.8.1", - "finalhandler": "~1.1.2", - "fresh": "0.5.2", - "merge-descriptors": "1.0.1", - "methods": "~1.1.2", - "on-finished": "~2.3.0", + "depd": "~2.0.0", + "on-headers": "~1.0.2", "parseurl": "~1.3.3", - "path-to-regexp": "0.1.7", - "proxy-addr": "~2.0.5", - "qs": "6.7.0", - "range-parser": "~1.2.1", - "safe-buffer": "5.1.2", - "send": "0.17.1", - "serve-static": "1.14.1", - "setprototypeof": "1.1.1", - "statuses": "~1.5.0", - "type-is": "~1.6.18", - "utils-merge": "1.0.1", - "vary": "~1.1.2" + "safe-buffer": "5.2.0", + "uid-safe": "~2.1.5" }, "dependencies": { - "body-parser": { - "version": "1.19.0", - "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.19.0.tgz", - "integrity": "sha512-dhEPs72UPbDnAQJ9ZKMNTP6ptJaionhP5cBb541nXPlW60Jepo9RV/a4fX4XWW9CuFNK22krhrj1+rgzifNCsw==", - "requires": { - "bytes": "3.1.0", - "content-type": "~1.0.4", - "debug": "2.6.9", - "depd": "~1.1.2", - "http-errors": "1.7.2", - "iconv-lite": "0.4.24", - "on-finished": "~2.3.0", - "qs": "6.7.0", - "raw-body": "2.4.0", - "type-is": "~1.6.17" - } + "depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==" + }, + "safe-buffer": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.0.tgz", + "integrity": "sha512-fZEwUGbVl7kouZs1jCdMLdt95hdIv0ZeHg6L7qPeciMZhZ+/gdesW4wgTARkrFWEpspjEATAzUGPG8N2jJiwbg==" } } }, - "extend": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", - "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==", - "optional": true - }, - "extsprintf": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.3.0.tgz", - "integrity": "sha1-lpGEQOMEGnpBT4xS48V06zw+HgU=", - "optional": true - }, - "fast-deep-equal": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", - "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", - "optional": true - }, - "fast-json-stable-stringify": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", - "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", - "optional": true - }, - "finalhandler": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.1.2.tgz", - "integrity": "sha512-aAWcW57uxVNrQZqFXjITpW3sIUQmHGG3qSb9mUah9MgMC4NeWhNOlNjXEYq3HjRAvL6arUviZGGJsBg6z0zsWA==", - "requires": { - "debug": "2.6.9", - "encodeurl": "~1.0.2", - "escape-html": "~1.0.3", - "on-finished": "~2.3.0", - "parseurl": "~1.3.3", - "statuses": "~1.5.0", - "unpipe": "~1.0.0" - } - }, "find-up": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/find-up/-/find-up-3.0.0.tgz", "integrity": "sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg==", - "dev": true, "requires": { "locate-path": "^3.0.0" } }, - "forever-agent": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/forever-agent/-/forever-agent-0.6.1.tgz", - "integrity": "sha1-+8cfDEGt6zf5bFd60e1C2P2sypE=", - "optional": true - }, - "form-data": { - "version": "2.3.3", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.3.3.tgz", - "integrity": "sha512-1lLKB2Mu3aGP1Q/2eCOx0fNbRMe7XdwktwOruhfqqd0rIJWwN4Dh+E3hrPSlDCXnSR7UtZ1N38rVXm+6+MEhJQ==", - "optional": true, - "requires": { - "asynckit": "^0.4.0", - "combined-stream": "^1.0.6", - "mime-types": "^2.1.12" - } - }, - "forwarded": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.1.2.tgz", - "integrity": "sha1-mMI9qxF1ZXuMBXPozszZGw/xjIQ=" - }, - "fresh": { - "version": "0.5.2", - "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", - "integrity": "sha1-PYyt2Q2XZWn6g1qx+OSyOhBWBac=" - }, - "fs-minipass": { - "version": "1.2.7", - "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-1.2.7.tgz", - "integrity": "sha512-GWSSJGFy4e9GUeCcbIkED+bgAoFyj7XF1mV8rma3QW4NIqX9Kyx79N/PF61H5udOV3aY1IaMLs6pGbH71nlCTA==", - "requires": { - "minipass": "^2.6.0" - } - }, - "fs.realpath": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", - "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=" - }, - "fstream": { - "version": "1.0.12", - "resolved": "https://registry.npmjs.org/fstream/-/fstream-1.0.12.tgz", - "integrity": "sha512-WvJ193OHa0GHPEL+AycEJgxvBEwyfRkN1vhjca23OaPVMCaLCXTd5qAu82AjTcgP1UJmytkOKb63Ypde7raDIg==", - "optional": true, - "requires": { - "graceful-fs": "^4.1.2", - "inherits": "~2.0.0", - "mkdirp": ">=0.5 0", - "rimraf": "2" - } - }, - "gauge": { - "version": "2.7.4", - "resolved": "https://registry.npmjs.org/gauge/-/gauge-2.7.4.tgz", - "integrity": "sha1-LANAXHU4w51+s3sxcCLjJfsBi/c=", - "requires": { - "aproba": "^1.0.3", - "console-control-strings": "^1.0.0", - "has-unicode": "^2.0.0", - "object-assign": "^4.1.0", - "signal-exit": "^3.0.0", - "string-width": "^1.0.1", - "strip-ansi": "^3.0.1", - "wide-align": "^1.1.0" - }, - "dependencies": { - "is-fullwidth-code-point": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-1.0.0.tgz", - "integrity": "sha1-754xOG8DGn8NZDr4L95QxFfvAMs=", - "requires": { - "number-is-nan": "^1.0.0" - } - }, - "string-width": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-1.0.2.tgz", - "integrity": "sha1-EYvfW4zcUaKn5w0hHgfisLmxB9M=", - "requires": { - "code-point-at": "^1.0.0", - "is-fullwidth-code-point": "^1.0.0", - "strip-ansi": "^3.0.0" - } - } - } - }, "get-caller-file": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-1.0.3.tgz", - "integrity": "sha512-3t6rVToeoZfYSGd8YoLFR2DJkiQrIiUrGcjvFX2mDw3bn6k2OtwHN0TNCLbBO+w8qTvimhDkv+LSscbJY1vE6w==", - "dev": true + "integrity": "sha512-3t6rVToeoZfYSGd8YoLFR2DJkiQrIiUrGcjvFX2mDw3bn6k2OtwHN0TNCLbBO+w8qTvimhDkv+LSscbJY1vE6w==" }, "get-stream": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-4.1.0.tgz", "integrity": "sha512-GMat4EJ5161kIy2HevLlr4luNjBgvmj413KaQA7jt4V8B4RDsfpHk7WQ9GVqfYyyx8OS/L66Kox+rJRNklLK7w==", - "dev": true, "requires": { "pump": "^3.0.0" } }, - "getpass": { - "version": "0.1.7", - "resolved": "https://registry.npmjs.org/getpass/-/getpass-0.1.7.tgz", - "integrity": "sha1-Xv+OPmhNVprkyysSgmBOi6YhSfo=", - "optional": true, - "requires": { - "assert-plus": "^1.0.0" - } - }, - "glob": { - "version": "7.1.6", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.6.tgz", - "integrity": "sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA==", - "requires": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.0.4", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" - } - }, - "graceful-fs": { - "version": "4.2.4", - "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.4.tgz", - "integrity": "sha512-WjKPNJF79dtJAVniUlGGWHYGz2jWxT6VhN/4m1NdkbZ2nOsEF+cI1Edgql5zCRhs/VsQYRvrXctxktVXZUkixw==", - "optional": true - }, - "har-schema": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/har-schema/-/har-schema-2.0.0.tgz", - "integrity": "sha1-qUwiJOvKwEeCoNkDVSHyRzW37JI=", - "optional": true - }, - "har-validator": { - "version": "5.1.3", - "resolved": "https://registry.npmjs.org/har-validator/-/har-validator-5.1.3.tgz", - "integrity": "sha512-sNvOCzEQNr/qrvJgc3UG/kD4QtlHycrzwS+6mfTrrSq97BvaYcPZZI1ZSqGSPR73Cxn4LKTD4PttRwfU7jWq5g==", - "optional": true, - "requires": { - "ajv": "^6.5.5", - "har-schema": "^2.0.0" - } - }, "has-flag": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", - "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=", - "dev": true - }, - "has-unicode": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/has-unicode/-/has-unicode-2.0.1.tgz", - "integrity": "sha1-4Ob+aijPUROIVeCG0Wkedx3iqLk=" + "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=" }, "hosted-git-info": { "version": "2.8.8", "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.8.8.tgz", - "integrity": "sha512-f/wzC2QaWBs7t9IYqB4T3sR1xviIViXJRJTWBlx2Gf3g0Xi5vI7Yy4koXQ1c9OYDGHN9sBy1DQ2AB8fqZBWhUg==", - "dev": true - }, - "http-errors": { - "version": "1.7.2", - "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.7.2.tgz", - "integrity": "sha512-uUQBt3H/cSIVfch6i1EuPNy/YsRSOUBXTVfZ+yR7Zjez3qjBz6i9+i4zjNaoqcoFVI4lQJ5plg63TvGfRSDCRg==", - "requires": { - "depd": "~1.1.2", - "inherits": "2.0.3", - "setprototypeof": "1.1.1", - "statuses": ">= 1.5.0 < 2", - "toidentifier": "1.0.0" - } - }, - "http-signature": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/http-signature/-/http-signature-1.2.0.tgz", - "integrity": "sha1-muzZJRFHcvPZW2WmCruPfBj7rOE=", - "optional": true, - "requires": { - "assert-plus": "^1.0.0", - "jsprim": "^1.2.2", - "sshpk": "^1.7.0" - } - }, - "iconv-lite": { - "version": "0.4.24", - "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", - "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", - "requires": { - "safer-buffer": ">= 2.1.2 < 3" - } - }, - "ignore-walk": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/ignore-walk/-/ignore-walk-3.0.3.tgz", - "integrity": "sha512-m7o6xuOaT1aqheYHKf8W6J5pYH85ZI9w077erOzLje3JsB1gkafkAhHHY19dqjulgIZHFm32Cp5uNZgcQqdJKw==", - "requires": { - "minimatch": "^3.0.4" - } - }, - "inflection": { - "version": "1.12.0", - "resolved": "https://registry.npmjs.org/inflection/-/inflection-1.12.0.tgz", - "integrity": "sha1-ogCTVlbW9fa8TcdQLhrstwMihBY=" - }, - "inflight": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", - "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=", - "requires": { - "once": "^1.3.0", - "wrappy": "1" - } - }, - "inherits": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", - "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=" - }, - "ini": { - "version": "1.3.5", - "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.5.tgz", - "integrity": "sha512-RZY5huIKCMRWDUqZlEi72f/lmXKMvuszcMBduliQ3nnWbx9X/ZBQO7DijMEYS9EhHBb2qacRUMtC7svLwe0lcw==" + "integrity": "sha512-f/wzC2QaWBs7t9IYqB4T3sR1xviIViXJRJTWBlx2Gf3g0Xi5vI7Yy4koXQ1c9OYDGHN9sBy1DQ2AB8fqZBWhUg==" }, "invert-kv": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/invert-kv/-/invert-kv-2.0.0.tgz", - "integrity": "sha512-wPVv/y/QQ/Uiirj/vh3oP+1Ww+AWehmi1g5fFWGPF6IpCBCDVrhgHRMvrLfdYcwDh3QJbGXDW4JAuzxElLSqKA==", - "dev": true - }, - "ipaddr.js": { - "version": "1.9.1", - "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", - "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==" + "integrity": "sha512-wPVv/y/QQ/Uiirj/vh3oP+1Ww+AWehmi1g5fFWGPF6IpCBCDVrhgHRMvrLfdYcwDh3QJbGXDW4JAuzxElLSqKA==" }, "is-arrayish": { "version": "0.2.1", "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", - "integrity": "sha1-d8mYQFJ6qOyxqLppe4BkWnqSap0=", - "dev": true + "integrity": "sha1-d8mYQFJ6qOyxqLppe4BkWnqSap0=" }, "is-fullwidth-code-point": { "version": "2.0.0", @@ -791,78 +252,22 @@ "is-stream": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-1.1.0.tgz", - "integrity": "sha1-EtSj3U5o4Lec6428hBc66A2RykQ=", - "dev": true - }, - "is-typedarray": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz", - "integrity": "sha1-5HnICFjfDBsR3dppQPlgEfzaSpo=", - "optional": true - }, - "isarray": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", - "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=" + "integrity": "sha1-EtSj3U5o4Lec6428hBc66A2RykQ=" }, "isexe": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", "integrity": "sha1-6PvzdNxVb/iUehDcsFctYz8s+hA=" }, - "isstream": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz", - "integrity": "sha1-R+Y/evVa+m+S4VAOaQ64uFKcCZo=", - "optional": true - }, - "jsbn": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-0.1.1.tgz", - "integrity": "sha1-peZUwuWi3rXyAdls77yoDA7y9RM=", - "optional": true - }, "json-parse-better-errors": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/json-parse-better-errors/-/json-parse-better-errors-1.0.2.tgz", - "integrity": "sha512-mrqyZKfX5EhL7hvqcV6WG1yYjnjeuYDzDhhcAAUrq8Po85NBQBJP+ZDUT75qZQ98IkUoBqdkExkukOU7Ts2wrw==", - "dev": true - }, - "json-schema": { - "version": "0.2.3", - "resolved": "https://registry.npmjs.org/json-schema/-/json-schema-0.2.3.tgz", - "integrity": "sha1-tIDIkuWaLwWVTOcnvT8qTogvnhM=", - "optional": true - }, - "json-schema-traverse": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", - "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", - "optional": true - }, - "json-stringify-safe": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", - "integrity": "sha1-Epai1Y/UXxmg9s4B1lcB4sc1tus=", - "optional": true - }, - "jsprim": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/jsprim/-/jsprim-1.4.1.tgz", - "integrity": "sha1-MT5mvB5cwG5Di8G3SZwuXFastqI=", - "optional": true, - "requires": { - "assert-plus": "1.0.0", - "extsprintf": "1.3.0", - "json-schema": "0.2.3", - "verror": "1.10.0" - } + "integrity": "sha512-mrqyZKfX5EhL7hvqcV6WG1yYjnjeuYDzDhhcAAUrq8Po85NBQBJP+ZDUT75qZQ98IkUoBqdkExkukOU7Ts2wrw==" }, "lcid": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/lcid/-/lcid-2.0.0.tgz", "integrity": "sha512-avPEb8P8EGnwXKClwsNUgryVjllcRqtMYa49NTsbQagYuT1DcXnl1915oxWjoyGrXR6zH/Y0Zc96xWsPcoDKeA==", - "dev": true, "requires": { "invert-kv": "^2.0.0" } @@ -871,7 +276,6 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-3.0.0.tgz", "integrity": "sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A==", - "dev": true, "requires": { "p-locate": "^3.0.0", "path-exists": "^3.0.0" @@ -886,242 +290,39 @@ "version": "0.1.3", "resolved": "https://registry.npmjs.org/map-age-cleaner/-/map-age-cleaner-0.1.3.tgz", "integrity": "sha512-bJzx6nMoP6PDLPBFmg7+xRKeFZvFboMrGlxmNj9ClvX53KrmvM5bXFXEWjbz4cz1AFn+jWJ9z/DJSz7hrs0w3w==", - "dev": true, "requires": { "p-defer": "^1.0.0" } }, - "media-typer": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", - "integrity": "sha1-hxDXrwqmJvj/+hzgAWhUUmMlV0g=" - }, "mem": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/mem/-/mem-4.3.0.tgz", "integrity": "sha512-qX2bG48pTqYRVmDB37rn/6PT7LcR8T7oAX3bf99u1Tt1nzxYfxkgqDwUwolPlXweM0XzBOBFzSx4kfp7KP1s/w==", - "dev": true, "requires": { "map-age-cleaner": "^0.1.1", "mimic-fn": "^2.0.0", "p-is-promise": "^2.0.0" } }, - "merge-descriptors": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz", - "integrity": "sha1-sAqqVW3YtEVoFQ7J0blT8/kMu2E=" - }, - "methods": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", - "integrity": "sha1-VSmk1nZUE07cxSZmVoNbD4Ua/O4=" - }, - "mime": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", - "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==" - }, - "mime-db": { - "version": "1.44.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.44.0.tgz", - "integrity": "sha512-/NOTfLrsPBVeH7YtFPgsVWveuL+4SjjYxaQ1xtM1KMFj7HdxlBlxeyNLzhyJVx7r4rZGJAZ/6lkKCitSc/Nmpg==" - }, - "mime-types": { - "version": "2.1.27", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.27.tgz", - "integrity": "sha512-JIhqnCasI9yD+SsmkquHBxTSEuZdQX5BuQnS2Vc7puQQQ+8yiP5AY5uWhpdv4YL4VM5c6iliiYWPgJ/nJQLp7w==", - "requires": { - "mime-db": "1.44.0" - } - }, "mimic-fn": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", - "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", - "dev": true - }, - "minimatch": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", - "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", - "requires": { - "brace-expansion": "^1.1.7" - } - }, - "minimist": { - "version": "1.2.5", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz", - "integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==" - }, - "minipass": { - "version": "2.9.0", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-2.9.0.tgz", - "integrity": "sha512-wxfUjg9WebH+CUDX/CdbRlh5SmfZiy/hpkxaRI16Y9W56Pa75sWgd/rvFilSgrauD9NyFymP/+JFV3KwzIsJeg==", - "requires": { - "safe-buffer": "^5.1.2", - "yallist": "^3.0.0" - } - }, - "minizlib": { - "version": "1.3.3", - "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-1.3.3.tgz", - "integrity": "sha512-6ZYMOEnmVsdCeTJVE0W9ZD+pVnE8h9Hma/iOwwRDsdQoePpoX56/8B6z3P9VNwppJuBKNRuFDRNRqRWexT9G9Q==", - "requires": { - "minipass": "^2.9.0" - } - }, - "mkdirp": { - "version": "0.5.5", - "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.5.tgz", - "integrity": "sha512-NKmAlESf6jMGym1++R0Ra7wvhV+wFW63FaSOFPwRahvea0gMUcGUhVeAg/0BC0wiv9ih5NYPB1Wn1UEI1/L+xQ==", - "requires": { - "minimist": "^1.2.5" - } - }, - "moment": { - "version": "2.27.0", - "resolved": "https://registry.npmjs.org/moment/-/moment-2.27.0.tgz", - "integrity": "sha512-al0MUK7cpIcglMv3YF13qSgdAIqxHTO7brRtaz3DlSULbqfazqkc5kEjNrLDOM7fsjshoFIihnU8snrP7zUvhQ==" - }, - "moment-timezone": { - "version": "0.5.31", - "resolved": "https://registry.npmjs.org/moment-timezone/-/moment-timezone-0.5.31.tgz", - "integrity": "sha512-+GgHNg8xRhMXfEbv81iDtrVeTcWt0kWmTEY1XQK14dICTXnWJnT0dxdlPspwqF3keKMVPXwayEsk1DI0AA/jdA==", - "requires": { - "moment": ">= 2.9.0" - } + "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==" }, "ms": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" }, - "needle": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/needle/-/needle-2.5.0.tgz", - "integrity": "sha512-o/qITSDR0JCyCKEQ1/1bnUXMmznxabbwi/Y4WwJElf+evwJNFNwIDMCCt5IigFVxgeGBJESLohGtIS9gEzo1fA==", - "requires": { - "debug": "^3.2.6", - "iconv-lite": "^0.4.4", - "sax": "^1.2.4" - }, - "dependencies": { - "debug": { - "version": "3.2.6", - "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.6.tgz", - "integrity": "sha512-mel+jf7nrtEl5Pn1Qx46zARXKDpBbvzezse7p7LqINmdoIk8PYP5SySaxEmYv6TZ0JyEKA1hsCId6DIhgITtWQ==", - "requires": { - "ms": "^2.1.1" - } - }, - "ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" - } - } - }, - "negotiator": { - "version": "0.6.2", - "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.2.tgz", - "integrity": "sha512-hZXc7K2e+PgeI1eDBe/10Ard4ekbfrrqG8Ep+8Jmf4JID2bNg7NvCPOZN+kfF574pFQI7mum2AUqDidoKqcTOw==" - }, "nice-try": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/nice-try/-/nice-try-1.0.5.tgz", - "integrity": "sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ==", - "dev": true - }, - "node-addon-api": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-2.0.0.tgz", - "integrity": "sha512-ASCL5U13as7HhOExbT6OlWJJUV/lLzL2voOSP1UVehpRD8FbSrSDjfScK/KwAvVTI5AS6r4VwbOMlIqtvRidnA==" - }, - "node-gyp": { - "version": "3.8.0", - "resolved": "https://registry.npmjs.org/node-gyp/-/node-gyp-3.8.0.tgz", - "integrity": "sha512-3g8lYefrRRzvGeSowdJKAKyks8oUpLEd/DyPV4eMhVlhJ0aNaZqIrNUIPuEWWTAoPqyFkfGrM67MC69baqn6vA==", - "optional": true, - "requires": { - "fstream": "^1.0.0", - "glob": "^7.0.3", - "graceful-fs": "^4.1.2", - "mkdirp": "^0.5.0", - "nopt": "2 || 3", - "npmlog": "0 || 1 || 2 || 3 || 4", - "osenv": "0", - "request": "^2.87.0", - "rimraf": "2", - "semver": "~5.3.0", - "tar": "^2.0.0", - "which": "1" - }, - "dependencies": { - "semver": { - "version": "5.3.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.3.0.tgz", - "integrity": "sha1-myzl094C0XxgEq0yaqa00M9U+U8=", - "optional": true - } - } - }, - "node-pre-gyp": { - "version": "0.11.0", - "resolved": "https://registry.npmjs.org/node-pre-gyp/-/node-pre-gyp-0.11.0.tgz", - "integrity": "sha512-TwWAOZb0j7e9eGaf9esRx3ZcLaE5tQ2lvYy1pb5IAaG1a2e2Kv5Lms1Y4hpj+ciXJRofIxxlt5haeQ/2ANeE0Q==", - "requires": { - "detect-libc": "^1.0.2", - "mkdirp": "^0.5.1", - "needle": "^2.2.1", - "nopt": "^4.0.1", - "npm-packlist": "^1.1.6", - "npmlog": "^4.0.2", - "rc": "^1.2.7", - "rimraf": "^2.6.1", - "semver": "^5.3.0", - "tar": "^4" - }, - "dependencies": { - "nopt": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/nopt/-/nopt-4.0.3.tgz", - "integrity": "sha512-CvaGwVMztSMJLOeXPrez7fyfObdZqNUK1cPAEzLHrTybIua9pMdmmPR5YwtfNftIOMv3DPUhFaxsZMNTQO20Kg==", - "requires": { - "abbrev": "1", - "osenv": "^0.1.4" - } - }, - "tar": { - "version": "4.4.13", - "resolved": "https://registry.npmjs.org/tar/-/tar-4.4.13.tgz", - "integrity": "sha512-w2VwSrBoHa5BsSyH+KxEqeQBAllHhccyMFVHtGtdMpF4W7IRWfZjFiQceJPChOeTsSDVUpER2T8FA93pr0L+QA==", - "requires": { - "chownr": "^1.1.1", - "fs-minipass": "^1.2.5", - "minipass": "^2.8.6", - "minizlib": "^1.2.1", - "mkdirp": "^0.5.0", - "safe-buffer": "^5.1.2", - "yallist": "^3.0.3" - } - } - } - }, - "nopt": { - "version": "3.0.6", - "resolved": "https://registry.npmjs.org/nopt/-/nopt-3.0.6.tgz", - "integrity": "sha1-xkZdvwirzU2zWTF/eaxopkayj/k=", - "optional": true, - "requires": { - "abbrev": "1" - } + "integrity": "sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ==" }, "normalize-package-data": { "version": "2.5.0", "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-2.5.0.tgz", "integrity": "sha512-/5CMN3T0R4XTj4DcGaexo+roZSdSFW/0AOOTROrjxzCG1wrWXEsGbRKevjlIL+ZDE4sZlJr5ED4YW0yqmkK+eA==", - "dev": true, "requires": { "hosted-git-info": "^2.1.4", "resolve": "^1.10.0", @@ -1129,72 +330,23 @@ "validate-npm-package-license": "^3.0.1" } }, - "npm-bundled": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/npm-bundled/-/npm-bundled-1.1.1.tgz", - "integrity": "sha512-gqkfgGePhTpAEgUsGEgcq1rqPXA+tv/aVBlgEzfXwA1yiUJF7xtEt3CtVwOjNYQOVknDk0F20w58Fnm3EtG0fA==", - "requires": { - "npm-normalize-package-bin": "^1.0.1" - } - }, - "npm-normalize-package-bin": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/npm-normalize-package-bin/-/npm-normalize-package-bin-1.0.1.tgz", - "integrity": "sha512-EPfafl6JL5/rU+ot6P3gRSCpPDW5VmIzX959Ob1+ySFUuuYHWHekXpwdUZcKP5C+DS4GEtdJluwBjnsNDl+fSA==" - }, - "npm-packlist": { - "version": "1.4.8", - "resolved": "https://registry.npmjs.org/npm-packlist/-/npm-packlist-1.4.8.tgz", - "integrity": "sha512-5+AZgwru5IevF5ZdnFglB5wNlHG1AOOuw28WhUq8/8emhBmLv6jX5by4WJCh7lW0uSYZYS6DXqIsyZVIXRZU9A==", - "requires": { - "ignore-walk": "^3.0.1", - "npm-bundled": "^1.0.1", - "npm-normalize-package-bin": "^1.0.1" - } - }, "npm-run-path": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-2.0.2.tgz", "integrity": "sha1-NakjLfo11wZ7TLLd8jV7GHFTbF8=", - "dev": true, "requires": { "path-key": "^2.0.0" } }, - "npmlog": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/npmlog/-/npmlog-4.1.2.tgz", - "integrity": "sha512-2uUqazuKlTaSI/dC8AzicUck7+IrEaOnN/e0jd3Xtt1KcGpwx30v50mL7oPyr/h9bL3E4aZccVwpwP+5W9Vjkg==", - "requires": { - "are-we-there-yet": "~1.1.2", - "console-control-strings": "~1.1.0", - "gauge": "~2.7.3", - "set-blocking": "~2.0.0" - } - }, "number-is-nan": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/number-is-nan/-/number-is-nan-1.0.1.tgz", "integrity": "sha1-CXtgK1NCKlIsGvuHkDGDNpQaAR0=" }, - "oauth-sign": { - "version": "0.9.0", - "resolved": "https://registry.npmjs.org/oauth-sign/-/oauth-sign-0.9.0.tgz", - "integrity": "sha512-fexhUFFPTGV8ybAtSIGbV6gOkSv8UtRbDBnAyLQw4QPKkgNlsH2ByPGtMUqdWkos6YCRmAqViwgZrJc/mRDzZQ==", - "optional": true - }, - "object-assign": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", - "integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=" - }, - "on-finished": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz", - "integrity": "sha1-IPEzZIGwg811M3mSoWlxqi2QaUc=", - "requires": { - "ee-first": "1.1.1" - } + "on-headers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/on-headers/-/on-headers-1.0.2.tgz", + "integrity": "sha512-pZAE+FJLoyITytdqK0U5s+FIpjN0JP3OzFi/u8Rx+EV5/W+JTWGXG8xFzevE7AjBfDqHv/8vL8qQsIhHnqRkrA==" }, "once": { "version": "1.4.0", @@ -1204,59 +356,35 @@ "wrappy": "1" } }, - "os-homedir": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/os-homedir/-/os-homedir-1.0.2.tgz", - "integrity": "sha1-/7xJiDNuDoM94MFox+8VISGqf7M=" - }, "os-locale": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/os-locale/-/os-locale-3.1.0.tgz", "integrity": "sha512-Z8l3R4wYWM40/52Z+S265okfFj8Kt2cC2MKY+xNi3kFs+XGI7WXu/I309QQQYbRW4ijiZ+yxs9pqEhJh0DqW3Q==", - "dev": true, "requires": { "execa": "^1.0.0", "lcid": "^2.0.0", "mem": "^4.0.0" } }, - "os-tmpdir": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz", - "integrity": "sha1-u+Z0BseaqFxc/sdm/lc0VV36EnQ=" - }, - "osenv": { - "version": "0.1.5", - "resolved": "https://registry.npmjs.org/osenv/-/osenv-0.1.5.tgz", - "integrity": "sha512-0CWcCECdMVc2Rw3U5w9ZjqX6ga6ubk1xDVKxtBQPK7wis/0F2r9T6k4ydGYhecl7YUBxBVxhL5oisPsNxAPe2g==", - "requires": { - "os-homedir": "^1.0.0", - "os-tmpdir": "^1.0.0" - } - }, "p-defer": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/p-defer/-/p-defer-1.0.0.tgz", - "integrity": "sha1-n26xgvbJqozXQwBKfU+WsZaw+ww=", - "dev": true + "integrity": "sha1-n26xgvbJqozXQwBKfU+WsZaw+ww=" }, "p-finally": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/p-finally/-/p-finally-1.0.0.tgz", - "integrity": "sha1-P7z7FbiZpEEjs0ttzBi3JDNqLK4=", - "dev": true + "integrity": "sha1-P7z7FbiZpEEjs0ttzBi3JDNqLK4=" }, "p-is-promise": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/p-is-promise/-/p-is-promise-2.1.0.tgz", - "integrity": "sha512-Y3W0wlRPK8ZMRbNq97l4M5otioeA5lm1z7bkNkxCka8HSPjR0xRWmpCmc9utiaLP9Jb1eD8BgeIxTW4AIF45Pg==", - "dev": true + "integrity": "sha512-Y3W0wlRPK8ZMRbNq97l4M5otioeA5lm1z7bkNkxCka8HSPjR0xRWmpCmc9utiaLP9Jb1eD8BgeIxTW4AIF45Pg==" }, "p-limit": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", - "dev": true, "requires": { "p-try": "^2.0.0" } @@ -1265,7 +393,6 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-3.0.0.tgz", "integrity": "sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ==", - "dev": true, "requires": { "p-limit": "^2.0.0" } @@ -1273,14 +400,12 @@ "p-try": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", - "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", - "dev": true + "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==" }, "parse-json": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-4.0.0.tgz", "integrity": "sha1-vjX1Qlvh9/bHRxhPmKeIy5lHfuA=", - "dev": true, "requires": { "error-ex": "^1.3.1", "json-parse-better-errors": "^1.0.1" @@ -1294,342 +419,87 @@ "path-exists": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-3.0.0.tgz", - "integrity": "sha1-zg6+ql94yxiSXqfYENe1mwEP1RU=", - "dev": true - }, - "path-is-absolute": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", - "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=" + "integrity": "sha1-zg6+ql94yxiSXqfYENe1mwEP1RU=" }, "path-key": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/path-key/-/path-key-2.0.1.tgz", - "integrity": "sha1-QRyttXTFoUDTpLGRDUDYDMn0C0A=", - "dev": true + "integrity": "sha1-QRyttXTFoUDTpLGRDUDYDMn0C0A=" }, "path-parse": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.6.tgz", - "integrity": "sha512-GSmOT2EbHrINBf9SR7CDELwlJ8AENk3Qn7OikK4nFYAu3Ote2+JYNVvkpAEQm3/TLNEJFD/xZJjzyxg3KBWOzw==", - "dev": true - }, - "path-to-regexp": { - "version": "0.1.7", - "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz", - "integrity": "sha1-32BBeABfUi8V60SQ5yR6G/qmf4w=" - }, - "performance-now": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz", - "integrity": "sha1-Ywn04OX6kT7BxpMHrjZLSzd8nns=", - "optional": true + "integrity": "sha512-GSmOT2EbHrINBf9SR7CDELwlJ8AENk3Qn7OikK4nFYAu3Ote2+JYNVvkpAEQm3/TLNEJFD/xZJjzyxg3KBWOzw==" }, "pify": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/pify/-/pify-3.0.0.tgz", - "integrity": "sha1-5aSs0sEB/fPZpNB/DbxNtJ3SgXY=", - "dev": true - }, - "process-nextick-args": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", - "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==" - }, - "proxy-addr": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.6.tgz", - "integrity": "sha512-dh/frvCBVmSsDYzw6n926jv974gddhkFPfiN8hPOi30Wax25QZyZEGveluCgliBnqmuM+UJmBErbAUFIoDbjOw==", - "requires": { - "forwarded": "~0.1.2", - "ipaddr.js": "1.9.1" - } - }, - "psl": { - "version": "1.8.0", - "resolved": "https://registry.npmjs.org/psl/-/psl-1.8.0.tgz", - "integrity": "sha512-RIdOzyoavK+hA18OGGWDqUTsCLhtA7IcZ/6NCs4fFJaHBDab+pDDmDIByWFRQJq2Cd7r1OoQxBGKOaztq+hjIQ==", - "optional": true + "integrity": "sha1-5aSs0sEB/fPZpNB/DbxNtJ3SgXY=" }, "pump": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz", "integrity": "sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==", - "dev": true, "requires": { "end-of-stream": "^1.1.0", "once": "^1.3.1" } }, - "punycode": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz", - "integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==", - "optional": true - }, - "qs": { - "version": "6.7.0", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.7.0.tgz", - "integrity": "sha512-VCdBRNFTX1fyE7Nb6FYoURo/SPe62QCaAyzJvUjwRaIsc+NePBEniHlvxFmmX56+HZphIGtV0XeCirBtpDrTyQ==" - }, - "range-parser": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", - "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==" - }, - "raw-body": { - "version": "2.4.0", - "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.4.0.tgz", - "integrity": "sha512-4Oz8DUIwdvoa5qMJelxipzi/iJIi40O5cGV1wNYp5hvZP8ZN0T+jiNkL0QepXs+EsQ9XJ8ipEDoiH70ySUJP3Q==", - "requires": { - "bytes": "3.1.0", - "http-errors": "1.7.2", - "iconv-lite": "0.4.24", - "unpipe": "1.0.0" - } - }, - "rc": { - "version": "1.2.8", - "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz", - "integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==", - "requires": { - "deep-extend": "^0.6.0", - "ini": "~1.3.0", - "minimist": "^1.2.0", - "strip-json-comments": "~2.0.1" - } + "random-bytes": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/random-bytes/-/random-bytes-1.0.0.tgz", + "integrity": "sha1-T2ih3Arli9P7lYSMMDJNt11kNgs=" }, "read-pkg": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-4.0.1.tgz", "integrity": "sha1-ljYlN48+HE1IyFhytabsfV0JMjc=", - "dev": true, "requires": { "normalize-package-data": "^2.3.2", "parse-json": "^4.0.0", "pify": "^3.0.0" } }, - "readable-stream": { - "version": "2.3.7", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz", - "integrity": "sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==", - "requires": { - "core-util-is": "~1.0.0", - "inherits": "~2.0.3", - "isarray": "~1.0.0", - "process-nextick-args": "~2.0.0", - "safe-buffer": "~5.1.1", - "string_decoder": "~1.1.1", - "util-deprecate": "~1.0.1" - } - }, - "request": { - "version": "2.88.2", - "resolved": "https://registry.npmjs.org/request/-/request-2.88.2.tgz", - "integrity": "sha512-MsvtOrfG9ZcrOwAW+Qi+F6HbD0CWXEh9ou77uOb7FM2WPhwT7smM833PzanhJLsgXjN89Ir6V2PczXNnMpwKhw==", - "optional": true, - "requires": { - "aws-sign2": "~0.7.0", - "aws4": "^1.8.0", - "caseless": "~0.12.0", - "combined-stream": "~1.0.6", - "extend": "~3.0.2", - "forever-agent": "~0.6.1", - "form-data": "~2.3.2", - "har-validator": "~5.1.3", - "http-signature": "~1.2.0", - "is-typedarray": "~1.0.0", - "isstream": "~0.1.2", - "json-stringify-safe": "~5.0.1", - "mime-types": "~2.1.19", - "oauth-sign": "~0.9.0", - "performance-now": "^2.1.0", - "qs": "~6.5.2", - "safe-buffer": "^5.1.2", - "tough-cookie": "~2.5.0", - "tunnel-agent": "^0.6.0", - "uuid": "^3.3.2" - }, - "dependencies": { - "qs": { - "version": "6.5.2", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.5.2.tgz", - "integrity": "sha512-N5ZAX4/LxJmF+7wN74pUD6qAh9/wnvdQcjq9TZjevvXzSUo7bfmw91saqMjzGS2xq91/odN2dW/WOl7qQHNDGA==", - "optional": true - }, - "uuid": { - "version": "3.4.0", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.4.0.tgz", - "integrity": "sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==", - "optional": true - } - } - }, "require-directory": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", - "integrity": "sha1-jGStX9MNqxyXbiNE/+f3kqam30I=", - "dev": true + "integrity": "sha1-jGStX9MNqxyXbiNE/+f3kqam30I=" }, "require-main-filename": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-1.0.1.tgz", - "integrity": "sha1-l/cXtp1IeE9fUmpsWqj/3aBVpNE=", - "dev": true + "integrity": "sha1-l/cXtp1IeE9fUmpsWqj/3aBVpNE=" }, "resolve": { "version": "1.17.0", "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.17.0.tgz", "integrity": "sha512-ic+7JYiV8Vi2yzQGFWOkiZD5Z9z7O2Zhm9XMaTxdJExKasieFCr+yXZ/WmXsckHiKl12ar0y6XiXDx3m4RHn1w==", - "dev": true, "requires": { "path-parse": "^1.0.6" } }, - "retry-as-promised": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/retry-as-promised/-/retry-as-promised-3.2.0.tgz", - "integrity": "sha512-CybGs60B7oYU/qSQ6kuaFmRd9sTZ6oXSc0toqePvV74Ac6/IFZSI1ReFQmtCN+uvW1Mtqdwpvt/LGOiCBAY2Mg==", - "requires": { - "any-promise": "^1.3.0" - } - }, - "rimraf": { - "version": "2.7.1", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.7.1.tgz", - "integrity": "sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==", - "requires": { - "glob": "^7.1.3" - } - }, "rxjs": { "version": "6.6.0", "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-6.6.0.tgz", "integrity": "sha512-3HMA8z/Oz61DUHe+SdOiQyzIf4tOx5oQHmMir7IZEu6TMqCLHT4LRcmNaUS0NwOz8VLvmmBduMsoaUvMaIiqzg==", - "dev": true, "requires": { "tslib": "^1.9.0" } }, - "safe-buffer": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", - "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" - }, - "safer-buffer": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", - "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" - }, - "sax": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/sax/-/sax-1.2.4.tgz", - "integrity": "sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw==" - }, "semver": { "version": "5.7.1", "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==" }, - "send": { - "version": "0.17.1", - "resolved": "https://registry.npmjs.org/send/-/send-0.17.1.tgz", - "integrity": "sha512-BsVKsiGcQMFwT8UxypobUKyv7irCNRHk1T0G680vk88yf6LBByGcZJOTJCrTP2xVN6yI+XjPJcNuE3V4fT9sAg==", - "requires": { - "debug": "2.6.9", - "depd": "~1.1.2", - "destroy": "~1.0.4", - "encodeurl": "~1.0.2", - "escape-html": "~1.0.3", - "etag": "~1.8.1", - "fresh": "0.5.2", - "http-errors": "~1.7.2", - "mime": "1.6.0", - "ms": "2.1.1", - "on-finished": "~2.3.0", - "range-parser": "~1.2.1", - "statuses": "~1.5.0" - }, - "dependencies": { - "ms": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.1.tgz", - "integrity": "sha512-tgp+dl5cGk28utYktBsrFqA7HKgrhgPsg6Z/EfhWI4gl1Hwq8B/GmY/0oXZ6nF8hDVesS/FpnYaD/kOWhYQvyg==" - } - } - }, - "sequelize": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/sequelize/-/sequelize-6.3.0.tgz", - "integrity": "sha512-aVZUvT0w1ebewlApFuaUJE/fJ7aTfIpMnwNM/Zgr29QnY0fT1t0EjXxl48Fwmfq3BHJogLMhfMTJRXJQaiaFVQ==", - "requires": { - "debug": "^4.1.1", - "dottie": "^2.0.0", - "inflection": "1.12.0", - "lodash": "^4.17.15", - "moment": "^2.26.0", - "moment-timezone": "^0.5.31", - "retry-as-promised": "^3.2.0", - "semver": "^7.3.2", - "sequelize-pool": "^6.0.0", - "toposort-class": "^1.0.1", - "uuid": "^8.1.0", - "validator": "^10.11.0", - "wkx": "^0.5.0" - }, - "dependencies": { - "debug": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz", - "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==", - "requires": { - "ms": "^2.1.1" - } - }, - "ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" - }, - "semver": { - "version": "7.3.2", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.2.tgz", - "integrity": "sha512-OrOb32TeeambH6UrhtShmF7CRDqhL6/5XpPNp2DuRH6+9QLw/orhp72j87v8Qa1ScDkvrrBNpZcDejAirJmfXQ==" - } - } - }, - "sequelize-pool": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/sequelize-pool/-/sequelize-pool-6.0.0.tgz", - "integrity": "sha512-D/VfOX2Z+6JTWqM73lhcqMXp1X4CeqRNVMlndvbOMtyjFAZ2kYzH7rGFGFrLO1r+RZQdc/h+3zQL4nd3cclNLg==" - }, - "serve-static": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.14.1.tgz", - "integrity": "sha512-JMrvUwE54emCYWlTI+hGrGv5I8dEwmco/00EvkzIIsR7MqrHonbD9pO2MOfFnpFntl7ecpZs+3mW+XbQZu9QCg==", - "requires": { - "encodeurl": "~1.0.2", - "escape-html": "~1.0.3", - "parseurl": "~1.3.3", - "send": "0.17.1" - } - }, "set-blocking": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", "integrity": "sha1-BF+XgtARrppoA93TgrJDkrPYkPc=" }, - "setprototypeof": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.1.1.tgz", - "integrity": "sha512-JvdAWfbXeIGaZ9cILp38HntZSFSo3mWg6xGcJJsd+d4aRMOqauag1C63dJfDw7OaMYwEbHMOxEZ1lqVRYP2OAw==" - }, "shebang-command": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-1.2.0.tgz", "integrity": "sha1-RKrGW2lbAzmJaMOfNj/uXer98eo=", - "dev": true, "requires": { "shebang-regex": "^1.0.0" } @@ -1637,8 +507,7 @@ "shebang-regex": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-1.0.0.tgz", - "integrity": "sha1-2kL0l0DAtC2yypcoVxyxkMmO/qM=", - "dev": true + "integrity": "sha1-2kL0l0DAtC2yypcoVxyxkMmO/qM=" }, "signal-exit": { "version": "3.0.3", @@ -1648,14 +517,12 @@ "spawn-command": { "version": "0.0.2-1", "resolved": "https://registry.npmjs.org/spawn-command/-/spawn-command-0.0.2-1.tgz", - "integrity": "sha1-YvXpRmmBwbeW3Fkpk34RycaSG9A=", - "dev": true + "integrity": "sha1-YvXpRmmBwbeW3Fkpk34RycaSG9A=" }, "spdx-correct": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/spdx-correct/-/spdx-correct-3.1.1.tgz", "integrity": "sha512-cOYcUWwhCuHCXi49RhFRCyJEK3iPj1Ziz9DpViV3tbZOwXD49QzIN3MpOLJNxh2qwq2lJJZaKMVw9qNi4jTC0w==", - "dev": true, "requires": { "spdx-expression-parse": "^3.0.0", "spdx-license-ids": "^3.0.0" @@ -1664,14 +531,12 @@ "spdx-exceptions": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/spdx-exceptions/-/spdx-exceptions-2.3.0.tgz", - "integrity": "sha512-/tTrYOC7PPI1nUAgx34hUpqXuyJG+DTHJTnIULG4rDygi4xu/tfgmq1e1cIRwRzwZgo4NLySi+ricLkZkw4i5A==", - "dev": true + "integrity": "sha512-/tTrYOC7PPI1nUAgx34hUpqXuyJG+DTHJTnIULG4rDygi4xu/tfgmq1e1cIRwRzwZgo4NLySi+ricLkZkw4i5A==" }, "spdx-expression-parse": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/spdx-expression-parse/-/spdx-expression-parse-3.0.1.tgz", "integrity": "sha512-cbqHunsQWnJNE6KhVSMsMeH5H/L9EpymbzqTQ3uLwNCLZ1Q481oWaofqH7nO6V07xlXwY6PhQdQ2IedWx/ZK4Q==", - "dev": true, "requires": { "spdx-exceptions": "^2.1.0", "spdx-license-ids": "^3.0.0" @@ -1680,40 +545,7 @@ "spdx-license-ids": { "version": "3.0.5", "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.5.tgz", - "integrity": "sha512-J+FWzZoynJEXGphVIS+XEh3kFSjZX/1i9gFBaWQcB+/tmpe2qUsSBABpcxqxnAxFdiUFEgAX1bjYGQvIZmoz9Q==", - "dev": true - }, - "sqlite3": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/sqlite3/-/sqlite3-5.0.0.tgz", - "integrity": "sha512-rjvqHFUaSGnzxDy2AHCwhHy6Zp6MNJzCPGYju4kD8yi6bze4d1/zMTg6C7JI49b7/EM7jKMTvyfN/4ylBKdwfw==", - "requires": { - "node-addon-api": "2.0.0", - "node-gyp": "3.x", - "node-pre-gyp": "^0.11.0" - } - }, - "sshpk": { - "version": "1.16.1", - "resolved": "https://registry.npmjs.org/sshpk/-/sshpk-1.16.1.tgz", - "integrity": "sha512-HXXqVUq7+pcKeLqqZj6mHFUMvXtOJt1uoUx09pFW6011inTMxqI8BA8PM95myrIyyKwdnzjdFjLiE6KBPVtJIg==", - "optional": true, - "requires": { - "asn1": "~0.2.3", - "assert-plus": "^1.0.0", - "bcrypt-pbkdf": "^1.0.0", - "dashdash": "^1.12.0", - "ecc-jsbn": "~0.1.1", - "getpass": "^0.1.1", - "jsbn": "~0.1.0", - "safer-buffer": "^2.0.2", - "tweetnacl": "~0.14.0" - } - }, - "statuses": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz", - "integrity": "sha1-Fhx9rBd2Wf2YEfQ3cfqZOBR4Yow=" + "integrity": "sha512-J+FWzZoynJEXGphVIS+XEh3kFSjZX/1i9gFBaWQcB+/tmpe2qUsSBABpcxqxnAxFdiUFEgAX1bjYGQvIZmoz9Q==" }, "string-width": { "version": "2.1.1", @@ -1739,14 +571,6 @@ } } }, - "string_decoder": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", - "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", - "requires": { - "safe-buffer": "~5.1.0" - } - }, "strip-ansi": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", @@ -1758,19 +582,12 @@ "strip-eof": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/strip-eof/-/strip-eof-1.0.0.tgz", - "integrity": "sha1-u0P/VZim6wXYm1n80SnJgzE2Br8=", - "dev": true - }, - "strip-json-comments": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", - "integrity": "sha1-PFMZQukIwml8DsNEhYwobHygpgo=" + "integrity": "sha1-u0P/VZim6wXYm1n80SnJgzE2Br8=" }, "supports-color": { "version": "4.5.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-4.5.0.tgz", "integrity": "sha1-vnoN5ITexcXN34s9WRJQRJEvY1s=", - "dev": true, "requires": { "has-flag": "^2.0.0" }, @@ -1778,138 +595,37 @@ "has-flag": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-2.0.0.tgz", - "integrity": "sha1-6CB68cx7MNRGzHC3NLXovhj4jVE=", - "dev": true + "integrity": "sha1-6CB68cx7MNRGzHC3NLXovhj4jVE=" } } }, - "tar": { - "version": "2.2.2", - "resolved": "https://registry.npmjs.org/tar/-/tar-2.2.2.tgz", - "integrity": "sha512-FCEhQ/4rE1zYv9rYXJw/msRqsnmlje5jHP6huWeBZ704jUTy02c5AZyWujpMR1ax6mVw9NyJMfuK2CMDWVIfgA==", - "optional": true, - "requires": { - "block-stream": "*", - "fstream": "^1.0.12", - "inherits": "2" - } - }, - "toidentifier": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.0.tgz", - "integrity": "sha512-yaOH/Pk/VEhBWWTlhI+qXxDFXlejDGcQipMlyxda9nthulaxLZUNcUqFxokp0vcYnvteJln5FNQDRrxj3YcbVw==" - }, - "toposort-class": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/toposort-class/-/toposort-class-1.0.1.tgz", - "integrity": "sha1-f/0feMi+KMO6Rc1OGj9e4ZO9mYg=" - }, - "tough-cookie": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.5.0.tgz", - "integrity": "sha512-nlLsUzgm1kfLXSXfRZMc1KLAugd4hqJHDTvc2hDIwS3mZAfMEuMbc03SujMF+GEcpaX/qboeycw6iO8JwVv2+g==", - "optional": true, - "requires": { - "psl": "^1.1.28", - "punycode": "^2.1.1" - } - }, "tree-kill": { "version": "1.2.2", "resolved": "https://registry.npmjs.org/tree-kill/-/tree-kill-1.2.2.tgz", - "integrity": "sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==", - "dev": true + "integrity": "sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==" }, "tslib": { "version": "1.13.0", "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.13.0.tgz", - "integrity": "sha512-i/6DQjL8Xf3be4K/E6Wgpekn5Qasl1usyw++dAA35Ue5orEn65VIxOA+YvNNl9HV3qv70T7CNwjODHZrLwvd1Q==", - "dev": true - }, - "tunnel-agent": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", - "integrity": "sha1-J6XeoGs2sEoKmWZ3SykIaPD8QP0=", - "optional": true, - "requires": { - "safe-buffer": "^5.0.1" - } - }, - "tweetnacl": { - "version": "0.14.5", - "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz", - "integrity": "sha1-WuaBd/GS1EViadEIr6k/+HQ/T2Q=", - "optional": true + "integrity": "sha512-i/6DQjL8Xf3be4K/E6Wgpekn5Qasl1usyw++dAA35Ue5orEn65VIxOA+YvNNl9HV3qv70T7CNwjODHZrLwvd1Q==" }, - "type-is": { - "version": "1.6.18", - "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", - "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", + "uid-safe": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/uid-safe/-/uid-safe-2.1.5.tgz", + "integrity": "sha512-KPHm4VL5dDXKz01UuEd88Df+KzynaohSL9fBh096KWAxSKZQDI2uBrVqtvRM4rwrIrRRKsdLNML/lnaaVSRioA==", "requires": { - "media-typer": "0.3.0", - "mime-types": "~2.1.24" + "random-bytes": "~1.0.0" } }, - "unpipe": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", - "integrity": "sha1-sr9O6FFKrmFltIF4KdIbLvSZBOw=" - }, - "uri-js": { - "version": "4.2.2", - "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.2.2.tgz", - "integrity": "sha512-KY9Frmirql91X2Qgjry0Wd4Y+YTdrdZheS8TFwvkbLWf/G5KNJDCh6pKL5OZctEW4+0Baa5idK2ZQuELRwPznQ==", - "optional": true, - "requires": { - "punycode": "^2.1.0" - } - }, - "util-deprecate": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", - "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=" - }, - "utils-merge": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", - "integrity": "sha1-n5VxD1CiZ5R7LMwSR0HBAoQn5xM=" - }, - "uuid": { - "version": "8.2.0", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.2.0.tgz", - "integrity": "sha512-CYpGiFTUrmI6OBMkAdjSDM0k5h8SkkiTP4WAjQgDgNB1S3Ou9VBEvr6q0Kv2H1mMk7IWfxYGpMH5sd5AvcIV2Q==" - }, "validate-npm-package-license": { "version": "3.0.4", "resolved": "https://registry.npmjs.org/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz", "integrity": "sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew==", - "dev": true, "requires": { "spdx-correct": "^3.0.0", "spdx-expression-parse": "^3.0.0" } }, - "validator": { - "version": "10.11.0", - "resolved": "https://registry.npmjs.org/validator/-/validator-10.11.0.tgz", - "integrity": "sha512-X/p3UZerAIsbBfN/IwahhYaBbY68EN/UQBWHtsbXGT5bfrH/p4NQzUCG1kF/rtKaNpnJ7jAu6NGTdSNtyNIXMw==" - }, - "vary": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", - "integrity": "sha1-IpnwLG3tMNSllhsLn3RSShj2NPw=" - }, - "verror": { - "version": "1.10.0", - "resolved": "https://registry.npmjs.org/verror/-/verror-1.10.0.tgz", - "integrity": "sha1-OhBcoXBTr1XW4nDB+CiGguGNpAA=", - "optional": true, - "requires": { - "assert-plus": "^1.0.0", - "core-util-is": "1.0.2", - "extsprintf": "^1.2.0" - } - }, "which": { "version": "1.3.1", "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz", @@ -1921,30 +637,12 @@ "which-module": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/which-module/-/which-module-2.0.0.tgz", - "integrity": "sha1-2e8H3Od7mQK4o6j6SzHD4/fm6Ho=", - "dev": true - }, - "wide-align": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/wide-align/-/wide-align-1.1.3.tgz", - "integrity": "sha512-QGkOQc8XL6Bt5PwnsExKBPuMKBxnGxWWW3fU55Xt4feHozMUhdUMaBCk290qpm/wG5u/RSKzwdAC4i51YigihA==", - "requires": { - "string-width": "^1.0.2 || 2" - } - }, - "wkx": { - "version": "0.5.0", - "resolved": "https://registry.npmjs.org/wkx/-/wkx-0.5.0.tgz", - "integrity": "sha512-Xng/d4Ichh8uN4l0FToV/258EjMGU9MGcA0HV2d9B/ZpZB3lqQm7nkOdZdm5GhKtLLhAE7PiVQwN4eN+2YJJUg==", - "requires": { - "@types/node": "*" - } + "integrity": "sha1-2e8H3Od7mQK4o6j6SzHD4/fm6Ho=" }, "wrap-ansi": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-2.1.0.tgz", "integrity": "sha1-2Pw9KE3QV5T+hJc8rs3Rz4JP3YU=", - "dev": true, "requires": { "string-width": "^1.0.1", "strip-ansi": "^3.0.1" @@ -1954,7 +652,6 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-1.0.0.tgz", "integrity": "sha1-754xOG8DGn8NZDr4L95QxFfvAMs=", - "dev": true, "requires": { "number-is-nan": "^1.0.0" } @@ -1963,7 +660,6 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/string-width/-/string-width-1.0.2.tgz", "integrity": "sha1-EYvfW4zcUaKn5w0hHgfisLmxB9M=", - "dev": true, "requires": { "code-point-at": "^1.0.0", "is-fullwidth-code-point": "^1.0.0", @@ -1980,19 +676,12 @@ "y18n": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.0.tgz", - "integrity": "sha512-r9S/ZyXu/Xu9q1tYlpsLIsa3EeLXXk0VwlxqTcFRfg9EhMW+17kbt9G0NrgCmhGb5vT2hyhJZLfDGx+7+5Uj/w==", - "dev": true - }, - "yallist": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", - "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==" + "integrity": "sha512-r9S/ZyXu/Xu9q1tYlpsLIsa3EeLXXk0VwlxqTcFRfg9EhMW+17kbt9G0NrgCmhGb5vT2hyhJZLfDGx+7+5Uj/w==" }, "yargs": { "version": "12.0.5", "resolved": "https://registry.npmjs.org/yargs/-/yargs-12.0.5.tgz", "integrity": "sha512-Lhz8TLaYnxq/2ObqHDql8dX8CJi97oHxrjUcYtzKbbykPtVW9WB+poxI+NM2UIzsMgNCZTIf0AQwsjK5yMAqZw==", - "dev": true, "requires": { "cliui": "^4.0.0", "decamelize": "^1.2.0", @@ -2012,7 +701,6 @@ "version": "11.1.1", "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-11.1.1.tgz", "integrity": "sha512-C6kB/WJDiaxONLJQnF8ccx9SEeoTTLek8RVbaOIsrAUS8VrBEXfmeSnCZxygc+XC2sNMBIwOOnfcxiynjHsVSQ==", - "dev": true, "requires": { "camelcase": "^5.0.0", "decamelize": "^1.2.0" diff --git a/package.json b/package.json index f27a93e..a24106b 100644 --- a/package.json +++ b/package.json @@ -8,6 +8,7 @@ "start": "npm run-script dev" }, "dependencies": { - "concurrently": "^4.0.1" + "concurrently": "^4.0.1", + "express-session": "^1.17.1" } } diff --git a/scripts/gpm_retrieve/gpm_retrieve.py b/scripts/gpm_retrieve/gpm_retrieve.py index c20a02d..87fc9f3 100755 --- a/scripts/gpm_retrieve/gpm_retrieve.py +++ b/scripts/gpm_retrieve/gpm_retrieve.py @@ -1,36 +1,49 @@ #!/usr/bin/env python3 -from gmusicapi import Mobileclient +import Mobileclient from gmusicapi import argparse import sys import requests import json +import urllib.parse + +creds_path = sys.path[0] + '/mobileclient.cred' -creds_path=sys.path[0] + '/mobileclient.cred' def authenticate(api): creds = api.perform_oauth(storage_filepath=creds_path, open_browser=False) -def uploadLibrary(mudbase_api, songs): +def uploadLibrary(mudbase_api, mudbase_user, mudbase_password, songs): + # First, attempt to login and start a session. + s = requests.Session() + response = s.post(mudbase_api + '/login?username=' + + urllib.parse.quote(mudbase_user) + + '&password=' + + urllib.parse.quote(mudbase_password)) + + if response.status_code != 200: + print("Unable to log in to MuDBase API.") + # Helpers def getArtistStoreIds(song): if 'artistId' in song: - return [ song['artistId'][0] ] - return []; + return [song['artistId'][0]] + return [] + def getSongStoreIds(song): if 'storeId' in song: - return [ song['storeId'] ] + return [song['storeId']] return [] # Create GPM import tag - gpmTagIdResponse = requests.post(mudbase_api + '/tag', data = { + gpmTagIdResponse = s.post(mudbase_api + '/tag', data={ 'name': 'GPM Import' }).json() gpmTagId = gpmTagIdResponse['id'] print(f"Created tag \"GPM Import\", response: {gpmTagIdResponse}") # Create the root genre tag - genreRootResponse = requests.post(mudbase_api + '/tag', data = { + genreRootResponse = s.post(mudbase_api + '/tag', data={ 'name': 'Genre' }).json() genreRootTagId = genreRootResponse['id'] @@ -47,75 +60,80 @@ def uploadLibrary(mudbase_api, songs): # Determine artist properties. artist = { 'name': song['artist'], - 'storeLinks': [ 'https://play.google.com/music/m' + id for id in getArtistStoreIds(song) ], - 'tagIds': [ gpmTagId ] + 'storeLinks': ['https://play.google.com/music/m' + id for id in getArtistStoreIds(song)], + 'tagIds': [gpmTagId] } if 'artist' in song else None # Determine album properties. album = { 'name': song['album'], - 'tagIds': [ gpmTagId ] + 'tagIds': [gpmTagId] } if 'album' in song else None # Determine genre properties. genre = { 'name': song['genre'], - 'parentId': genreRootTagId + 'parentId': genreRootTagId } if 'genre' in song else None # Upload artist if not already done artistId = None if artist: - for key,value in storedArtists.items(): + for key, value in storedArtists.items(): if value == artist: artistId = key break if not artistId: - response = requests.post(mudbase_api + '/artist', json = artist).json() + response = s.post(mudbase_api + '/artist', json=artist).json() artistId = response['id'] - print(f"Created artist \"{artist['name']}\", response: {response}") + print( + f"Created artist \"{artist['name']}\", response: {response}") storedArtists[artistId] = artist # Upload album if not already done albumId = None if album: - for key,value in storedAlbums.items(): + for key, value in storedAlbums.items(): if value == album: albumId = key break if not albumId: - response = requests.post(mudbase_api + '/album', json = album).json() + response = s.post(mudbase_api + '/album', json=album).json() albumId = response['id'] - print(f"Created album \"{album['name']}\", response: {response}") + print( + f"Created album \"{album['name']}\", response: {response}") storedAlbums[albumId] = album # Upload genre if not already done genreTagId = None if genre: - for key,value in storedGenreTags.items(): + for key, value in storedGenreTags.items(): if value == genre: genreTagId = key break if not genreTagId: - response = requests.post(mudbase_api + '/tag', json = genre).json() + response = s.post(mudbase_api + '/tag', json=genre).json() genreTagId = response['id'] - print(f"Created genre tag \"Genre / {genre['name']}\", response: {response}") + print( + f"Created genre tag \"Genre / {genre['name']}\", response: {response}") storedGenreTags[genreTagId] = genre # Upload the song itself - tagIds = [ gpmTagId ] + tagIds = [gpmTagId] if genreTagId: tagIds.append(genreTagId) _song = { 'title': song['title'], - 'artistIds': [ artistId ] if artistId != None else [], - 'albumIds': [ albumId ] if albumId != None else [], + 'artistIds': [artistId] if artistId != None else [], + 'albumIds': [albumId] if albumId != None else [], 'tagIds': tagIds, - 'storeLinks': [ 'https://play.google.com/music/m/' + id for id in getSongStoreIds(song) ], + 'storeLinks': ['https://play.google.com/music/m/' + id for id in getSongStoreIds(song)], } - response = requests.post(mudbase_api + '/song', json = _song).json() - print(f"Created song \"{song['title']}\" with artist ID {artistId}, album ID {albumId}, response: {response}") - + response = s.post(mudbase_api + '/song', json=_song).json() + print( + f"Created song \"{song['title']}\" with artist ID {artistId}, album ID {albumId}, response: {response}") + + def getData(api): return { "songs": api.get_all_songs(), @@ -125,7 +143,7 @@ def getData(api): def getSongs(data): # Get songs from library - songs = [] #data['songs'] + songs = [] # data['songs'] # Append songs from playlists for playlist in data['playlists']: @@ -135,16 +153,27 @@ def getSongs(data): # Uniquify by using a dict. After all, same song may appear in # multiple playlists. - sI = lambda song: song['artist'] + '-' + song['title'] if 'artist' in song and 'title' in song else 'z' + def sI(song): return song['artist'] + '-' + \ + song['title'] if 'artist' in song and 'title' in song else 'z' return list(dict((sI(song), song) for song in songs).values()) + api = Mobileclient() -parser = argparse.ArgumentParser(description="Import Google Music library into MudBase.") -parser.add_argument('--authenticate', help="Generate credentials for authentication", action="store_true") -parser.add_argument('--store-to', help="Store GPM library to JSON for later upload", action='store', dest='store_to') -parser.add_argument('--load-from', help="Load GPM library from JSON for upload", action='store', dest='load_from') -parser.add_argument('--mudbase_api', help="Address for the Mudbase back-end API to upload to", action='store', dest='mudbase_api') +parser = argparse.ArgumentParser( + description="Import Google Music library into MudBase.") +parser.add_argument( + '--authenticate', help="Generate credentials for authentication", action="store_true") +parser.add_argument('--store-to', help="Store GPM library to JSON for later upload", + action='store', dest='store_to') +parser.add_argument('--load-from', help="Load GPM library from JSON for upload", + action='store', dest='load_from') +parser.add_argument('--mudbase_api', help="Address for the Mudbase back-end API to upload to", + action='store', dest='mudbase_api') +parser.add_argument('--mudbase_user', help="Username for the Mudbase API", + action='store', dest="mudbase_user") +parser.add_argument('--mudbase_password', help="Password for the Mudbase API", + action='store', dest="mudbase_password") args = parser.parse_args() @@ -155,7 +184,8 @@ data = None # Determine whether we need to log in to GPM and get songs if args.store_to or (not args.load_from and args.mudbase_api): - api.oauth_login(Mobileclient.FROM_MAC_ADDRESS, oauth_credentials=creds_path) + api.oauth_login(Mobileclient.FROM_MAC_ADDRESS, + oauth_credentials=creds_path) data = getData(api) # Determine whether to save to a file @@ -172,5 +202,7 @@ songs = getSongs(data) print(f"Found {len(songs)} songs.") if args.mudbase_api: - api.oauth_login(Mobileclient.FROM_MAC_ADDRESS, oauth_credentials=creds_path) - uploadLibrary(args.mudbase_api, songs) + api.oauth_login(Mobileclient.FROM_MAC_ADDRESS, + oauth_credentials=creds_path) + uploadLibrary(args.mudbase_api, args.mudbase_user, + args.mudbase_password, songs) diff --git a/server/app.ts b/server/app.ts index 3155d73..2411cb3 100644 --- a/server/app.ts +++ b/server/app.ts @@ -2,33 +2,39 @@ const bodyParser = require('body-parser'); import * as api from '../client/src/api'; import Knex from 'knex'; -import { CreateSongEndpointHandler } from './endpoints/CreateSongEndpointHandler'; -import { CreateArtistEndpointHandler } from './endpoints/CreateArtistEndpointHandler'; -import { QueryEndpointHandler } from './endpoints/QueryEndpointHandler'; -import { ArtistDetailsEndpointHandler } from './endpoints/ArtistDetailsEndpointHandler' -import { SongDetailsEndpointHandler } from './endpoints/SongDetailsEndpointHandler'; -import { ModifyArtistEndpointHandler } from './endpoints/ModifyArtistEndpointHandler'; -import { ModifySongEndpointHandler } from './endpoints/ModifySongEndpointHandler'; -import { CreateTagEndpointHandler } from './endpoints/CreateTagEndpointHandler'; -import { ModifyTagEndpointHandler } from './endpoints/ModifyTagEndpointHandler'; -import { TagDetailsEndpointHandler } from './endpoints/TagDetailsEndpointHandler'; -import { CreateAlbumEndpointHandler } from './endpoints/CreateAlbumEndpointHandler'; -import { ModifyAlbumEndpointHandler } from './endpoints/ModifyAlbumEndpointHandler'; -import { AlbumDetailsEndpointHandler } from './endpoints/AlbumDetailsEndpointHandler'; -import { DeleteTagEndpointHandler } from './endpoints/DeleteTagEndpointHandler'; -import { MergeTagEndpointHandler } from './endpoints/MergeTagEndpointHandler'; +import { CreateSongEndpointHandler } from './endpoints/CreateSong'; +import { CreateArtistEndpointHandler } from './endpoints/CreateArtist'; +import { QueryEndpointHandler } from './endpoints/Query'; +import { ArtistDetailsEndpointHandler } from './endpoints/ArtistDetails' +import { SongDetailsEndpointHandler } from './endpoints/SongDetails'; +import { ModifyArtistEndpointHandler } from './endpoints/ModifyArtist'; +import { ModifySongEndpointHandler } from './endpoints/ModifySong'; +import { CreateTagEndpointHandler } from './endpoints/CreateTag'; +import { ModifyTagEndpointHandler } from './endpoints/ModifyTag'; +import { TagDetailsEndpointHandler } from './endpoints/TagDetails'; +import { CreateAlbumEndpointHandler } from './endpoints/CreateAlbum'; +import { ModifyAlbumEndpointHandler } from './endpoints/ModifyAlbum'; +import { AlbumDetailsEndpointHandler } from './endpoints/AlbumDetails'; +import { DeleteTagEndpointHandler } from './endpoints/DeleteTag'; +import { MergeTagEndpointHandler } from './endpoints/MergeTag'; +import { RegisterUserEndpointHandler } from './endpoints/RegisterUser'; import * as endpointTypes from './endpoints/types'; +import { sha512 } from 'js-sha512'; -const invokeHandler = (handler:endpointTypes.EndpointHandler, knex: Knex) => { +// For authentication +var passport = require('passport'); +var Strategy = require('passport-local').Strategy; + +const invokeHandler = (handler: endpointTypes.EndpointHandler, knex: Knex) => { return async (req: any, res: any) => { console.log("Incoming", req.method, " @ ", req.url); await handler(req, res, knex) - .catch(endpointTypes.catchUnhandledErrors) - .catch((_e:endpointTypes.EndpointError) => { - let e:endpointTypes.EndpointError = _e; - console.log("Error handling request: ", e.internalMessage); - res.sendStatus(e.httpStatus); - }) + .catch(endpointTypes.catchUnhandledErrors) + .catch((_e: endpointTypes.EndpointError) => { + let e: endpointTypes.EndpointError = _e; + console.log("Error handling request: ", e.internalMessage); + res.sendStatus(e.httpStatus); + }) console.log("Finished handling", req.method, "@", req.url); }; } @@ -37,26 +43,84 @@ const SetupApp = (app: any, knex: Knex, apiBaseUrl: string) => { app.use(bodyParser.json()); app.use(bodyParser.urlencoded({ extended: true })); - const invokeWithKnex = (handler: endpointTypes.EndpointHandler) => { + // Set up auth. See: https://github.com/passport/express-4.x-local-example.git + passport.use(new Strategy( + function (email: string, password: string, cb: any) { + (async () => { + try { + const user = await knex.select(['email', 'passwordHash', 'id']) + .from('users') + .where({ 'email': email }) + .then((users: any) => users[0]); + if (!user) { cb(null, false); } + if (sha512(password) != user.passwordHash) { + return cb(null, false); + } + return cb(null, user); + } catch (error) { cb(error); } + })(); + })); + passport.serializeUser(function (user: any, cb: any) { + cb(null, user.id); + }); + passport.deserializeUser(function (id: number, cb: any) { + (async () => { + try { + const user = await knex.select(['email', 'passwordHash', 'id']) + .from('users') + .where({ 'id': id }) + .then((users: any) => users[0]); + if (!user) { cb(null, false); } + return cb(null, user); + } catch (error) { cb(error); } + })(); + }); + + app.use(require('express-session')({ secret: 'EA9q5cukt7UFhN', resave: false, saveUninitialized: false })); + app.use(passport.initialize()); + app.use(passport.session()); + + const _invoke = (handler: endpointTypes.EndpointHandler) => { return invokeHandler(handler, knex); } + const checkLogin = () => { + return function (req: any, res: any, next: any) { + if (!req.isAuthenticated || !req.isAuthenticated()) { + return res + .status(401) + .json({ reason: "NotLoggedIn" }) + .send(); + } + next(); + } + } + // Set up REST API endpoints - app.post(apiBaseUrl + api.CreateSongEndpoint, invokeWithKnex(CreateSongEndpointHandler)); - app.post(apiBaseUrl + api.QueryEndpoint, invokeWithKnex(QueryEndpointHandler)); - app.post(apiBaseUrl + api.CreateArtistEndpoint, invokeWithKnex(CreateArtistEndpointHandler)); - app.put(apiBaseUrl + api.ModifyArtistEndpoint, invokeWithKnex(ModifyArtistEndpointHandler)); - app.put(apiBaseUrl + api.ModifySongEndpoint, invokeWithKnex(ModifySongEndpointHandler)); - app.get(apiBaseUrl + api.SongDetailsEndpoint, invokeWithKnex(SongDetailsEndpointHandler)); - app.get(apiBaseUrl + api.ArtistDetailsEndpoint, invokeWithKnex(ArtistDetailsEndpointHandler)); - app.post(apiBaseUrl + api.CreateTagEndpoint, invokeWithKnex(CreateTagEndpointHandler)); - app.put(apiBaseUrl + api.ModifyTagEndpoint, invokeWithKnex(ModifyTagEndpointHandler)); - app.get(apiBaseUrl + api.TagDetailsEndpoint, invokeWithKnex(TagDetailsEndpointHandler)); - app.post(apiBaseUrl + api.CreateAlbumEndpoint, invokeWithKnex(CreateAlbumEndpointHandler)); - app.put(apiBaseUrl + api.ModifyAlbumEndpoint, invokeWithKnex(ModifyAlbumEndpointHandler)); - app.get(apiBaseUrl + api.AlbumDetailsEndpoint, invokeWithKnex(AlbumDetailsEndpointHandler)); - app.delete(apiBaseUrl + api.DeleteTagEndpoint, invokeWithKnex(DeleteTagEndpointHandler)); - app.post(apiBaseUrl + api.MergeTagEndpoint, invokeWithKnex(MergeTagEndpointHandler)); + app.post(apiBaseUrl + api.CreateSongEndpoint, checkLogin(), _invoke(CreateSongEndpointHandler)); + app.post(apiBaseUrl + api.QueryEndpoint, checkLogin(), _invoke(QueryEndpointHandler)); + app.post(apiBaseUrl + api.CreateArtistEndpoint, checkLogin(), _invoke(CreateArtistEndpointHandler)); + app.put(apiBaseUrl + api.ModifyArtistEndpoint, checkLogin(), _invoke(ModifyArtistEndpointHandler)); + app.put(apiBaseUrl + api.ModifySongEndpoint, checkLogin(), _invoke(ModifySongEndpointHandler)); + app.get(apiBaseUrl + api.SongDetailsEndpoint, checkLogin(), _invoke(SongDetailsEndpointHandler)); + app.get(apiBaseUrl + api.ArtistDetailsEndpoint, checkLogin(), _invoke(ArtistDetailsEndpointHandler)); + app.post(apiBaseUrl + api.CreateTagEndpoint, checkLogin(), _invoke(CreateTagEndpointHandler)); + app.put(apiBaseUrl + api.ModifyTagEndpoint, checkLogin(), _invoke(ModifyTagEndpointHandler)); + app.get(apiBaseUrl + api.TagDetailsEndpoint, checkLogin(), _invoke(TagDetailsEndpointHandler)); + app.post(apiBaseUrl + api.CreateAlbumEndpoint, checkLogin(), _invoke(CreateAlbumEndpointHandler)); + app.put(apiBaseUrl + api.ModifyAlbumEndpoint, checkLogin(), _invoke(ModifyAlbumEndpointHandler)); + app.get(apiBaseUrl + api.AlbumDetailsEndpoint, checkLogin(), _invoke(AlbumDetailsEndpointHandler)); + app.delete(apiBaseUrl + api.DeleteTagEndpoint, checkLogin(), _invoke(DeleteTagEndpointHandler)); + app.post(apiBaseUrl + api.MergeTagEndpoint, checkLogin(), _invoke(MergeTagEndpointHandler)); + app.post(apiBaseUrl + api.RegisterUserEndpoint, _invoke(RegisterUserEndpointHandler)); + + app.post('/login', passport.authenticate('local'), (req: any, res: any) => { + res.status(200).send({ userId: req.user.id }); + }); + app.post('/logout', function (req: any, res: any) { + req.logout(); + res.status(200).send(); + }); } export { SetupApp } \ No newline at end of file diff --git a/server/endpoints/AlbumDetailsEndpointHandler.ts b/server/endpoints/AlbumDetails.ts similarity index 78% rename from server/endpoints/AlbumDetailsEndpointHandler.ts rename to server/endpoints/AlbumDetails.ts index e833ff9..635b210 100644 --- a/server/endpoints/AlbumDetailsEndpointHandler.ts +++ b/server/endpoints/AlbumDetails.ts @@ -12,6 +12,8 @@ export const AlbumDetailsEndpointHandler: EndpointHandler = async (req: any, res throw e; } + const { id: userId } = req.user; + try { // Start transfers for songs, tags and artists. // Also request the album itself. @@ -35,6 +37,7 @@ export const AlbumDetailsEndpointHandler: EndpointHandler = async (req: any, res }); const albumPromise = knex.select('name', 'storeLinks') .from('albums') + .where({ 'user': userId }) .where({ id: req.params.id }) .then((albums: any) => albums[0]); @@ -43,17 +46,19 @@ export const AlbumDetailsEndpointHandler: EndpointHandler = async (req: any, res await Promise.all([albumPromise, tagIdsPromise, songIdsPromise, artistIdsPromise]); // Respond to the request. - console.log("ALBUM: ", album); - const response: api.AlbumDetailsResponse = { - name: album['name'], - artistIds: artists, - tagIds: tags, - songIds: songs, - storeLinks: asJson(album['storeLinks']), - }; - - await res.send(response); + if (album) { + const response: api.AlbumDetailsResponse = { + name: album['name'], + artistIds: artists, + tagIds: tags, + songIds: songs, + storeLinks: asJson(album['storeLinks']), + }; + await res.send(response); + } else { + await res.status(404).send({}); + } } catch (e) { - catchUnhandledErrors(e); - } + catchUnhandledErrors(e); +} } \ No newline at end of file diff --git a/server/endpoints/ArtistDetailsEndpointHandler.ts b/server/endpoints/ArtistDetails.ts similarity index 70% rename from server/endpoints/ArtistDetailsEndpointHandler.ts rename to server/endpoints/ArtistDetails.ts index ede0836..effe19f 100644 --- a/server/endpoints/ArtistDetailsEndpointHandler.ts +++ b/server/endpoints/ArtistDetails.ts @@ -12,6 +12,8 @@ export const ArtistDetailsEndpointHandler: EndpointHandler = async (req: any, re throw e; } + const { id: userId } = req.user; + try { const tagIds = Array.from(new Set((await knex.select('tagId') .from('artists_tags') @@ -20,15 +22,19 @@ export const ArtistDetailsEndpointHandler: EndpointHandler = async (req: any, re const results = await knex.select(['id', 'name', 'storeLinks']) .from('artists') + .where({ 'user': userId }) .where({ 'id': req.params.id }); - const response: api.ArtistDetailsResponse = { - name: results[0].name, - tagIds: tagIds, - storeLinks: asJson(results[0].storeLinks), + if (results[0]) { + const response: api.ArtistDetailsResponse = { + name: results[0].name, + tagIds: tagIds, + storeLinks: asJson(results[0].storeLinks), + } + await res.send(response); + } else { + await res.status(404).send({}); } - - await res.send(response); } catch (e) { catchUnhandledErrors(e) } diff --git a/server/endpoints/CreateAlbumEndpointHandler.ts b/server/endpoints/CreateAlbum.ts similarity index 93% rename from server/endpoints/CreateAlbumEndpointHandler.ts rename to server/endpoints/CreateAlbum.ts index e9f9509..7172d5b 100644 --- a/server/endpoints/CreateAlbumEndpointHandler.ts +++ b/server/endpoints/CreateAlbum.ts @@ -11,8 +11,9 @@ export const CreateAlbumEndpointHandler: EndpointHandler = async (req: any, res: throw e; } const reqObject: api.CreateAlbumRequest = req.body; + const { id: userId } = req.user; - console.log("Create Album:", reqObject); + console.log("User ", userId, ": Create Album ", reqObject); await knex.transaction(async (trx) => { try { @@ -20,6 +21,7 @@ export const CreateAlbumEndpointHandler: EndpointHandler = async (req: any, res: const artistIdsPromise = reqObject.artistIds ? trx.select('id') .from('artists') + .where({ 'user': userId }) .whereIn('id', reqObject.artistIds) .then((as: any) => as.map((a: any) => a['id'])) : (async () => { return [] })(); @@ -28,6 +30,7 @@ export const CreateAlbumEndpointHandler: EndpointHandler = async (req: any, res: const tagIdsPromise = reqObject.tagIds ? trx.select('id') .from('tags') + .where({ 'user': userId }) .whereIn('id', reqObject.tagIds) .then((as: any) => as.map((a: any) => a['id'])) : (async () => { return [] })(); @@ -50,6 +53,7 @@ export const CreateAlbumEndpointHandler: EndpointHandler = async (req: any, res: .insert({ name: reqObject.name, storeLinks: JSON.stringify(reqObject.storeLinks || []), + user: userId, }) .returning('id') // Needed for Postgres )[0]; diff --git a/server/endpoints/CreateArtistEndpointHandler.ts b/server/endpoints/CreateArtist.ts similarity index 92% rename from server/endpoints/CreateArtistEndpointHandler.ts rename to server/endpoints/CreateArtist.ts index 155e9d2..0496c47 100644 --- a/server/endpoints/CreateArtistEndpointHandler.ts +++ b/server/endpoints/CreateArtist.ts @@ -11,8 +11,9 @@ export const CreateArtistEndpointHandler: EndpointHandler = async (req: any, res throw e; } const reqObject: api.CreateArtistRequest = req.body; + const { id: userId } = req.user; - console.log("Create artist:", reqObject) + console.log("User ", userId, ": Create artist ", reqObject) await knex.transaction(async (trx) => { try { @@ -20,13 +21,12 @@ export const CreateArtistEndpointHandler: EndpointHandler = async (req: any, res const tags: number[] = reqObject.tagIds ? Array.from(new Set( (await trx.select('id').from('tags') + .where({ 'user': userId }) .whereIn('id', reqObject.tagIds)) .map((tag: any) => tag['id']) )) : []; - console.log("Found artist tags:", tags) - if (reqObject.tagIds && tags && tags.length !== reqObject.tagIds.length) { const e: EndpointError = { internalMessage: 'Not all tags exist for CreateArtist request: ' + JSON.stringify(req.body), @@ -40,6 +40,7 @@ export const CreateArtistEndpointHandler: EndpointHandler = async (req: any, res .insert({ name: reqObject.name, storeLinks: JSON.stringify(reqObject.storeLinks || []), + user: userId, }) .returning('id') // Needed for Postgres )[0]; diff --git a/server/endpoints/CreateSongEndpointHandler.ts b/server/endpoints/CreateSong.ts similarity index 93% rename from server/endpoints/CreateSongEndpointHandler.ts rename to server/endpoints/CreateSong.ts index 89fdf58..a3c80c1 100644 --- a/server/endpoints/CreateSongEndpointHandler.ts +++ b/server/endpoints/CreateSong.ts @@ -11,8 +11,9 @@ export const CreateSongEndpointHandler: EndpointHandler = async (req: any, res: throw e; } const reqObject: api.CreateSongRequest = req.body; + const { id: userId } = req.user; - console.log("Create Song:", reqObject); + console.log("User ", userId, ": Create Song ", reqObject); await knex.transaction(async (trx) => { try { @@ -20,6 +21,7 @@ export const CreateSongEndpointHandler: EndpointHandler = async (req: any, res: const artistIdsPromise = reqObject.artistIds ? trx.select('id') .from('artists') + .where({ 'user': userId }) .whereIn('id', reqObject.artistIds) .then((as: any) => as.map((a: any) => a['id'])) : (async () => { return [] })(); @@ -28,6 +30,7 @@ export const CreateSongEndpointHandler: EndpointHandler = async (req: any, res: const tagIdsPromise = reqObject.tagIds ? trx.select('id') .from('tags') + .where({ 'user': userId }) .whereIn('id', reqObject.tagIds) .then((as: any) => as.map((a: any) => a['id'])) : (async () => { return [] })(); @@ -36,6 +39,7 @@ export const CreateSongEndpointHandler: EndpointHandler = async (req: any, res: const albumIdsPromise = reqObject.albumIds ? trx.select('id') .from('albums') + .where({ 'user': userId }) .whereIn('id', reqObject.albumIds) .then((as: any) => as.map((a: any) => a['id'])) : (async () => { return [] })(); @@ -59,6 +63,7 @@ export const CreateSongEndpointHandler: EndpointHandler = async (req: any, res: .insert({ title: reqObject.title, storeLinks: JSON.stringify(reqObject.storeLinks || []), + user: userId, }) .returning('id') // Needed for Postgres )[0]; diff --git a/server/endpoints/CreateTagEndpointHandler.ts b/server/endpoints/CreateTag.ts similarity index 89% rename from server/endpoints/CreateTagEndpointHandler.ts rename to server/endpoints/CreateTag.ts index 183ed1c..1587bcf 100644 --- a/server/endpoints/CreateTagEndpointHandler.ts +++ b/server/endpoints/CreateTag.ts @@ -11,8 +11,9 @@ export const CreateTagEndpointHandler: EndpointHandler = async (req: any, res: a throw e; } const reqObject: api.CreateTagRequest = req.body; + const { id: userId } = req.user; - console.log("Create Tag: ", reqObject); + console.log("User ", userId, ": Create Tag ", reqObject); await knex.transaction(async (trx) => { try { @@ -21,6 +22,7 @@ export const CreateTagEndpointHandler: EndpointHandler = async (req: any, res: a reqObject.parentId ? (await trx.select('id') .from('tags') + .where({ 'user': userId }) .where({ 'id': reqObject.parentId }))[0]['id'] : undefined; @@ -35,7 +37,8 @@ export const CreateTagEndpointHandler: EndpointHandler = async (req: any, res: a // Create the new tag. var tag: any = { - name: reqObject.name + name: reqObject.name, + user: userId, }; if (maybeParent) { tag['parentId'] = maybeParent; diff --git a/server/endpoints/DeleteTagEndpointHandler.ts b/server/endpoints/DeleteTag.ts similarity index 83% rename from server/endpoints/DeleteTagEndpointHandler.ts rename to server/endpoints/DeleteTag.ts index 6d0ab94..3fd9ae3 100644 --- a/server/endpoints/DeleteTagEndpointHandler.ts +++ b/server/endpoints/DeleteTag.ts @@ -2,13 +2,14 @@ import * as api from '../../client/src/api'; import { EndpointError, EndpointHandler, catchUnhandledErrors } from './types'; import Knex from 'knex'; -async function getChildrenRecursive(id: number, trx: any) { +async function getChildrenRecursive(id: number, userId: number, trx: any) { const directChildren = (await trx.select('id') .from('tags') + .where({ 'user': userId }) .where({ 'parentId': id })).map((r: any) => r.id); const indirectChildrenPromises = directChildren.map( - (child: number) => getChildrenRecursive(child, trx) + (child: number) => getChildrenRecursive(child, userId, trx) ); const indirectChildrenNested = await Promise.all(indirectChildrenPromises); const indirectChildren = indirectChildrenNested.flat(); @@ -28,19 +29,20 @@ export const DeleteTagEndpointHandler: EndpointHandler = async (req: any, res: a throw e; } const reqObject: api.DeleteTagRequest = req.body; + const { id: userId } = req.user; - console.log("Delete Tag:", reqObject); + console.log("User ", userId, ": Delete Tag ", reqObject); await knex.transaction(async (trx) => { try { // Start retrieving any child tags. - const childTagsPromise = - getChildrenRecursive(req.params.id, trx); + getChildrenRecursive(req.params.id, userId, trx); // Start retrieving the tag itself. const tagPromise = trx.select('id') .from('tags') + .where({ 'user': userId }) .where({ id: req.params.id }) .then((r: any) => (r && r[0]) ? r[0]['id'] : undefined) @@ -49,7 +51,6 @@ export const DeleteTagEndpointHandler: EndpointHandler = async (req: any, res: a // Merge all IDs. const toDelete = [ tag, ...children ]; - console.log ("deleting tags: ", toDelete); // Check that we found all objects we need. if (!tag) { @@ -62,6 +63,7 @@ export const DeleteTagEndpointHandler: EndpointHandler = async (req: any, res: a // Delete the tag and its children. await trx('tags') + .where({ 'user': userId }) .whereIn('id', toDelete) .del(); diff --git a/server/endpoints/MergeTagEndpointHandler.ts b/server/endpoints/MergeTag.ts similarity index 90% rename from server/endpoints/MergeTagEndpointHandler.ts rename to server/endpoints/MergeTag.ts index b84db4b..ed776c5 100644 --- a/server/endpoints/MergeTagEndpointHandler.ts +++ b/server/endpoints/MergeTag.ts @@ -11,8 +11,9 @@ export const MergeTagEndpointHandler: EndpointHandler = async (req: any, res: an throw e; } const reqObject: api.DeleteTagRequest = req.body; + const { id: userId } = req.user; - console.log("Merge Tag:", reqObject); + console.log("User ", userId, ": Merge Tag ", reqObject); const fromId = req.params.id; const toId = req.params.toId; @@ -21,12 +22,14 @@ export const MergeTagEndpointHandler: EndpointHandler = async (req: any, res: an // Start retrieving the "from" tag. const fromTagPromise = trx.select('id') .from('tags') + .where({ 'user': userId }) .where({ id: fromId }) .then((r: any) => (r && r[0]) ? r[0]['id'] : undefined) // Start retrieving the "to" tag. const toTagPromise = trx.select('id') .from('tags') + .where({ 'user': userId }) .where({ id: toId }) .then((r: any) => (r && r[0]) ? r[0]['id'] : undefined) @@ -44,6 +47,7 @@ export const MergeTagEndpointHandler: EndpointHandler = async (req: any, res: an // Assign new tag ID to any objects referencing the to-be-merged tag. const cPromise = trx('tags') + .where({ 'user': userId }) .where({ 'parentId': fromId }) .update({ 'parentId': toId }); const sPromise = trx('songs_tags') @@ -59,6 +63,7 @@ export const MergeTagEndpointHandler: EndpointHandler = async (req: any, res: an // Delete the original tag. await trx('tags') + .where({ 'user': userId }) .where({ 'id': fromId }) .del(); diff --git a/server/endpoints/ModifyAlbumEndpointHandler.ts b/server/endpoints/ModifyAlbum.ts similarity index 96% rename from server/endpoints/ModifyAlbumEndpointHandler.ts rename to server/endpoints/ModifyAlbum.ts index 8d01eb0..829f6c2 100644 --- a/server/endpoints/ModifyAlbumEndpointHandler.ts +++ b/server/endpoints/ModifyAlbum.ts @@ -11,8 +11,9 @@ export const ModifyAlbumEndpointHandler: EndpointHandler = async (req: any, res: throw e; } const reqObject: api.ModifyAlbumRequest = req.body; + const { id: userId } = req.user; - console.log("Modify Album:", reqObject); + console.log("User ", userId, ": Modify Album ", reqObject); await knex.transaction(async (trx) => { try { @@ -20,6 +21,7 @@ export const ModifyAlbumEndpointHandler: EndpointHandler = async (req: any, res: // Start retrieving the album itself. const albumPromise = trx.select('id') .from('albums') + .where({ 'user': userId }) .where({ id: req.params.id }) .then((r: any) => (r && r[0]) ? r[0]['id'] : undefined); @@ -58,6 +60,7 @@ export const ModifyAlbumEndpointHandler: EndpointHandler = async (req: any, res: if ("name" in reqObject) { update["name"] = reqObject.name; } if ("storeLinks" in reqObject) { update["storeLinks"] = JSON.stringify(reqObject.storeLinks || []); } const modifyAlbumPromise = trx('albums') + .where({ 'user': userId }) .where({ 'id': req.params.id }) .update(update) diff --git a/server/endpoints/ModifyArtistEndpointHandler.ts b/server/endpoints/ModifyArtist.ts similarity index 95% rename from server/endpoints/ModifyArtistEndpointHandler.ts rename to server/endpoints/ModifyArtist.ts index b0cc77e..3d56ee6 100644 --- a/server/endpoints/ModifyArtistEndpointHandler.ts +++ b/server/endpoints/ModifyArtist.ts @@ -11,7 +11,9 @@ export const ModifyArtistEndpointHandler: EndpointHandler = async (req: any, res throw e; } const reqObject: api.ModifyArtistRequest = req.body; - console.log("Modify Artist:", reqObject); + const { id: userId } = req.user; + + console.log("User ", userId, ": Modify Artist ", reqObject); await knex.transaction(async (trx) => { try { @@ -20,6 +22,7 @@ export const ModifyArtistEndpointHandler: EndpointHandler = async (req: any, res // Start retrieving the artist itself. const artistPromise = trx.select('id') .from('artists') + .where({ 'user': userId }) .where({ id: artistId }) .then((r: any) => (r && r[0]) ? r[0]['id'] : undefined) @@ -49,6 +52,7 @@ export const ModifyArtistEndpointHandler: EndpointHandler = async (req: any, res if ("name" in reqObject) { update["name"] = reqObject.name; } if ("storeLinks" in reqObject) { update["storeLinks"] = JSON.stringify(reqObject.storeLinks || []); } const modifyArtistPromise = trx('artists') + .where({ 'user': userId }) .where({ 'id': artistId }) .update(update) diff --git a/server/endpoints/ModifySongEndpointHandler.ts b/server/endpoints/ModifySong.ts similarity index 97% rename from server/endpoints/ModifySongEndpointHandler.ts rename to server/endpoints/ModifySong.ts index 9a280f4..488b72f 100644 --- a/server/endpoints/ModifySongEndpointHandler.ts +++ b/server/endpoints/ModifySong.ts @@ -11,14 +11,16 @@ export const ModifySongEndpointHandler: EndpointHandler = async (req: any, res: throw e; } const reqObject: api.ModifySongRequest = req.body; + const { id: userId } = req.user; - console.log("Modify Song:", reqObject); + console.log("User ", userId, ": Modify Song ", reqObject); await knex.transaction(async (trx) => { try { // Retrieve the song to be modified itself. const songPromise = trx.select('id') .from('songs') + .where({ 'user': userId }) .where({ id: req.params.id }) .then((r: any) => (r && r[0]) ? r[0]['id'] : undefined) @@ -67,6 +69,7 @@ export const ModifySongEndpointHandler: EndpointHandler = async (req: any, res: if ("title" in reqObject) { update["title"] = reqObject.title; } if ("storeLinks" in reqObject) { update["storeLinks"] = JSON.stringify(reqObject.storeLinks || []); } const modifySongPromise = trx('songs') + .where({ 'user': userId }) .where({ 'id': req.params.id }) .update(update) diff --git a/server/endpoints/ModifyTagEndpointHandler.ts b/server/endpoints/ModifyTag.ts similarity index 90% rename from server/endpoints/ModifyTagEndpointHandler.ts rename to server/endpoints/ModifyTag.ts index adfb558..6596958 100644 --- a/server/endpoints/ModifyTagEndpointHandler.ts +++ b/server/endpoints/ModifyTag.ts @@ -11,8 +11,9 @@ export const ModifyTagEndpointHandler: EndpointHandler = async (req: any, res: a throw e; } const reqObject: api.ModifyTagRequest = req.body; + const { id: userId } = req.user; - console.log("Modify Tag:", reqObject); + console.log("User ", userId, ": Modify Tag ", reqObject); await knex.transaction(async (trx) => { try { @@ -20,6 +21,7 @@ export const ModifyTagEndpointHandler: EndpointHandler = async (req: any, res: a const parentTagPromise = reqObject.parentId ? trx.select('id') .from('tags') + .where({ 'user': userId }) .where({ 'id': reqObject.parentId }) .then((ts: any) => ts.map((t: any) => t['tagId'])) : (async () => { return [] })(); @@ -27,6 +29,7 @@ export const ModifyTagEndpointHandler: EndpointHandler = async (req: any, res: a // Start retrieving the tag itself. const tagPromise = trx.select('id') .from('tags') + .where({ 'user': userId }) .where({ id: req.params.id }) .then((r: any) => (r && r[0]) ? r[0]['id'] : undefined) @@ -45,6 +48,7 @@ export const ModifyTagEndpointHandler: EndpointHandler = async (req: any, res: a // Modify the tag. await trx('tags') + .where({ 'user': userId }) .where({ 'id': req.params.id }) .update({ name: reqObject.name, diff --git a/server/endpoints/QueryEndpointHandler.ts b/server/endpoints/Query.ts similarity index 92% rename from server/endpoints/QueryEndpointHandler.ts rename to server/endpoints/Query.ts index 89fb35d..60111f3 100644 --- a/server/endpoints/QueryEndpointHandler.ts +++ b/server/endpoints/Query.ts @@ -178,7 +178,7 @@ const objectColumns = { [ObjectType.Tag]: ['tags.id as tags.id', 'tags.name as tags.name', 'tags.parentId as tags.parentId'] }; -function constructQuery(knex: Knex, queryFor: ObjectType, queryElem: api.QueryElem, ordering: api.Ordering, +function constructQuery(knex: Knex, userId: number, queryFor: ObjectType, queryElem: api.QueryElem, ordering: api.Ordering, offset: number, limit: number | null) { const joinObjects = getRequiredDatabaseObjects(queryElem); joinObjects.delete(queryFor); // We are already querying this object in the base query. @@ -194,6 +194,7 @@ function constructQuery(knex: Knex, queryFor: ObjectType, queryElem: api.QueryEl // First, we create a base query for the type of object we need to yield. var q = knex.select(columns) + .where({ [objectTables[queryFor] + '.user']: userId }) .groupBy(objectTables[queryFor] + '.' + 'id') .from(objectTables[queryFor]); @@ -223,7 +224,7 @@ function constructQuery(knex: Knex, queryFor: ObjectType, queryElem: api.QueryEl return q; } -async function getLinkedObjects(knex: Knex, base: ObjectType, linked: ObjectType, baseIds: number[]) { +async function getLinkedObjects(knex: Knex, userId: number, base: ObjectType, linked: ObjectType, baseIds: number[]) { var result: Record = {}; const otherTable = objectTables[linked]; const linkingTable = getLinkingTable(base, linked); @@ -232,6 +233,7 @@ async function getLinkedObjects(knex: Knex, base: ObjectType, linked: ObjectType await Promise.all(baseIds.map((baseId: number) => { return knex.select(columns).groupBy(otherTable + '.id').from(otherTable) .join(linkingTable, { [linkingTable + '.' + linkingTableIdNames[linked]]: otherTable + '.id' }) + .where({ [otherTable + '.user']: userId }) .where({ [linkingTable + '.' + linkingTableIdNames[base]]: baseId }) .then((others: any) => { result[baseId] = others; }) })) @@ -241,11 +243,12 @@ async function getLinkedObjects(knex: Knex, base: ObjectType, linked: ObjectType } // Resolve a tag into the full nested structure of its ancestors. -async function getFullTag(knex: Knex, tag: any): Promise { +async function getFullTag(knex: Knex, userId: number, tag: any): Promise { const resolveTag = async (t: any) => { if (t['tags.parentId']) { const parent = (await knex.select(objectColumns[ObjectType.Tag]) .from('tags') + .where({ 'user': userId }) .where({ [objectTables[ObjectType.Tag] + '.id']: t['tags.parentId'] }))[0]; t.parent = await resolveTag(parent); } @@ -264,7 +267,9 @@ export const QueryEndpointHandler: EndpointHandler = async (req: any, res: any, throw e; } const reqObject: api.QueryRequest = req.body; - console.log("Query: ", reqObject); + const { id: userId } = req.user; + + console.log("User ", userId, ": Query ", reqObject); try { const songLimit = reqObject.offsetsLimits.songLimit; @@ -278,6 +283,7 @@ export const QueryEndpointHandler: EndpointHandler = async (req: any, res: any, const artistsPromise: Promise = (artistLimit && artistLimit !== 0) ? constructQuery(knex, + userId, ObjectType.Artist, reqObject.query, reqObject.ordering, @@ -288,6 +294,7 @@ export const QueryEndpointHandler: EndpointHandler = async (req: any, res: any, const albumsPromise: Promise = (albumLimit && albumLimit !== 0) ? constructQuery(knex, + userId, ObjectType.Album, reqObject.query, reqObject.ordering, @@ -298,6 +305,7 @@ export const QueryEndpointHandler: EndpointHandler = async (req: any, res: any, const songsPromise: Promise = (songLimit && songLimit !== 0) ? constructQuery(knex, + userId, ObjectType.Song, reqObject.query, reqObject.ordering, @@ -308,6 +316,7 @@ export const QueryEndpointHandler: EndpointHandler = async (req: any, res: any, const tagsPromise: Promise = (tagLimit && tagLimit !== 0) ? constructQuery(knex, + userId, ObjectType.Tag, reqObject.query, reqObject.ordering, @@ -325,18 +334,18 @@ export const QueryEndpointHandler: EndpointHandler = async (req: any, res: any, })(); const songsArtistsPromise: Promise> = (songLimit && songLimit !== 0) ? (async () => { - return await getLinkedObjects(knex, ObjectType.Song, ObjectType.Artist, await songIdsPromise); + return await getLinkedObjects(knex, userId, ObjectType.Song, ObjectType.Artist, await songIdsPromise); })() : (async () => { return {}; })(); const songsTagsPromise: Promise> = (songLimit && songLimit !== 0) ? (async () => { - const tagsPerSong: Record = await getLinkedObjects(knex, ObjectType.Song, ObjectType.Tag, await songIdsPromise); + const tagsPerSong: Record = await getLinkedObjects(knex, userId, ObjectType.Song, ObjectType.Tag, await songIdsPromise); var result: Record = {}; for (var key in tagsPerSong) { const tags = tagsPerSong[key]; var fullTags: any[] = []; for (var idx in tags) { - fullTags.push(await getFullTag(knex, tags[idx])); + fullTags.push(await getFullTag(knex, userId, tags[idx])); } result[key] = fullTags; } @@ -345,7 +354,7 @@ export const QueryEndpointHandler: EndpointHandler = async (req: any, res: any, (async () => { return {}; })(); const songsAlbumsPromise: Promise> = (songLimit && songLimit !== 0) ? (async () => { - return await getLinkedObjects(knex, ObjectType.Song, ObjectType.Album, await songIdsPromise); + return await getLinkedObjects(knex, userId, ObjectType.Song, ObjectType.Album, await songIdsPromise); })() : (async () => { return {}; })(); diff --git a/server/endpoints/RegisterUser.ts b/server/endpoints/RegisterUser.ts new file mode 100644 index 0000000..0782d90 --- /dev/null +++ b/server/endpoints/RegisterUser.ts @@ -0,0 +1,49 @@ +import * as api from '../../client/src/api'; +import { EndpointError, EndpointHandler, catchUnhandledErrors } from './types'; +import Knex from 'knex'; + +import { sha512 } from 'js-sha512'; + +export const RegisterUserEndpointHandler: EndpointHandler = async (req: any, res: any, knex: Knex) => { + if (!api.checkRegisterUserRequest(req)) { + const e: EndpointError = { + internalMessage: 'Invalid RegisterUser request: ' + JSON.stringify(req.body), + httpStatus: 400 + }; + throw e; + } + const reqObject: api.RegisterUserRequest = req.body; + + console.log("Register User: ", reqObject); + + await knex.transaction(async (trx) => { + try { + // check if the user already exists + const user = (await trx + .select('id') + .from('users') + .where({ email: reqObject.email }))[0]; + if(user) { + res.status(400).send(); + return; + } + + // Create the new user. + const passwordHash = sha512(reqObject.password); + const userId = (await trx('users') + .insert({ + email: reqObject.email, + passwordHash: passwordHash, + }) + .returning('id') // Needed for Postgres + )[0]; + + // Respond to the request. + res.status(200).send(); + + } catch (e) { + catchUnhandledErrors(e); + trx.rollback(); + } + }) +} \ No newline at end of file diff --git a/server/endpoints/SongDetailsEndpointHandler.ts b/server/endpoints/SongDetails.ts similarity index 80% rename from server/endpoints/SongDetailsEndpointHandler.ts rename to server/endpoints/SongDetails.ts index 591e31a..24e6267 100644 --- a/server/endpoints/SongDetailsEndpointHandler.ts +++ b/server/endpoints/SongDetails.ts @@ -12,6 +12,8 @@ export const SongDetailsEndpointHandler: EndpointHandler = async (req: any, res: throw e; } + const { id: userId } = req.user; + try { const tagIdsPromise: Promise = knex.select('tagId') .from('songs_tags') @@ -41,21 +43,25 @@ export const SongDetailsEndpointHandler: EndpointHandler = async (req: any, res: }) const songPromise = await knex.select(['id', 'title', 'storeLinks']) .from('songs') + .where({ 'user': userId }) .where({ 'id': req.params.id }) .then((ss: any) => ss[0]) const [tags, albums, artists, song] = await Promise.all([tagIdsPromise, albumIdsPromise, artistIdsPromise, songPromise]); - const response: api.SongDetailsResponse = { - title: song.title, - tagIds: tags, - artistIds: artists, - albumIds: albums, - storeLinks: asJson(song.storeLinks), + if (song) { + const response: api.SongDetailsResponse = { + title: song.title, + tagIds: tags, + artistIds: artists, + albumIds: albums, + storeLinks: asJson(song.storeLinks), + } + await res.send(response); + } else { + await res.status(404).send({}); } - - await res.send(response); } catch (e) { catchUnhandledErrors(e) } diff --git a/server/endpoints/TagDetailsEndpointHandler.ts b/server/endpoints/TagDetails.ts similarity index 65% rename from server/endpoints/TagDetailsEndpointHandler.ts rename to server/endpoints/TagDetails.ts index 7497acb..daacaae 100644 --- a/server/endpoints/TagDetailsEndpointHandler.ts +++ b/server/endpoints/TagDetails.ts @@ -11,17 +11,23 @@ export const TagDetailsEndpointHandler: EndpointHandler = async (req: any, res: throw e; } + const { id: userId } = req.user; + try { const results = await knex.select(['id', 'name', 'parentId']) .from('tags') + .where({ 'user': userId }) .where({ 'id': req.params.id }); - const response: api.TagDetailsResponse = { - name: results[0].name, - parentId: results[0].parentId, + if (results[0]) { + const response: api.TagDetailsResponse = { + name: results[0].name, + parentId: results[0].parentId || undefined, + } + await res.send(response); + } else { + await res.status(404).send({}); } - - await res.send(response); } catch (e) { catchUnhandledErrors(e) } diff --git a/server/migrations/20201110170100_add_users.ts b/server/migrations/20201110170100_add_users.ts new file mode 100644 index 0000000..6b75776 --- /dev/null +++ b/server/migrations/20201110170100_add_users.ts @@ -0,0 +1,73 @@ +import * as Knex from "knex"; +import { sha512 } from "js-sha512"; + + +export async function up(knex: Knex): Promise { + // Users table. + await knex.schema.createTable( + 'users', + (table: any) => { + table.increments('id'); + table.string('email'); + table.string('passwordHash') + } + ) + + // Add user column to other object tables. + await knex.schema.alterTable( + 'songs', + (table: any) => { + table.integer('user').unsigned().notNullable().defaultTo(1); + } + ) + await knex.schema.alterTable( + 'albums', + (table: any) => { + table.integer('user').unsigned().notNullable().defaultTo(1); + } + ) + await knex.schema.alterTable( + 'tags', + (table: any) => { + table.integer('user').unsigned().notNullable().defaultTo(1); + } + ) + await knex.schema.alterTable( + 'artists', + (table: any) => { + table.integer('user').unsigned().notNullable().defaultTo(1); + } + ) +} + + +export async function down(knex: Knex): Promise { + await knex.schema.dropTable('users'); + + // Remove the user column + await knex.schema.alterTable( + 'songs', + (table: any) => { + table.dropColumn('user'); + } + ) + await knex.schema.alterTable( + 'albums', + (table: any) => { + table.dropColumn('user'); + } + ) + await knex.schema.alterTable( + 'tags', + (table: any) => { + table.dropColumn('user'); + } + ) + await knex.schema.alterTable( + 'artists', + (table: any) => { + table.dropColumn('user'); + } + ) +} + diff --git a/server/package-lock.json b/server/package-lock.json index 6b6e850..a52e71e 100644 --- a/server/package-lock.json +++ b/server/package-lock.json @@ -1936,6 +1936,11 @@ "resolved": "https://registry.npmjs.org/jasmine-core/-/jasmine-core-3.5.0.tgz", "integrity": "sha512-nCeAiw37MIMA9w9IXso7bRaLl+c/ef3wnxsoSAlYrzS+Ot0zTG6nU8G/cIfGkqpkjX2wNaIW9RFG0TwIFnG6bA==" }, + "js-sha512": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/js-sha512/-/js-sha512-0.8.0.tgz", + "integrity": "sha512-PWsmefG6Jkodqt+ePTvBZCSMFgN7Clckjd0O7su3I0+BW2QWUTJNzjktHsztGLhncP2h8mcF9V9Y2Ha59pAViQ==" + }, "jsbi": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/jsbi/-/jsbi-3.1.3.tgz", @@ -2756,6 +2761,28 @@ "resolved": "https://registry.npmjs.org/pascalcase/-/pascalcase-0.1.1.tgz", "integrity": "sha1-s2PlXoAGym/iF4TS2yK9FdeRfxQ=" }, + "passport": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/passport/-/passport-0.4.1.tgz", + "integrity": "sha512-IxXgZZs8d7uFSt3eqNjM9NQ3g3uQCW5avD8mRNoXV99Yig50vjuaez6dQK2qC0kVWPRTujxY0dWgGfT09adjYg==", + "requires": { + "passport-strategy": "1.x.x", + "pause": "0.0.1" + } + }, + "passport-local": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/passport-local/-/passport-local-1.0.0.tgz", + "integrity": "sha1-H+YyaMkudWBmJkN+O5BmYsFbpu4=", + "requires": { + "passport-strategy": "1.x.x" + } + }, + "passport-strategy": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/passport-strategy/-/passport-strategy-1.0.0.tgz", + "integrity": "sha1-tVOaqPwiWj0a0XlHbd8ja0QPUuQ=" + }, "path-is-absolute": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", @@ -2789,6 +2816,11 @@ "resolved": "https://registry.npmjs.org/pathval/-/pathval-1.1.0.tgz", "integrity": "sha1-uULm1L3mUwBe9rcTYd74cn0GReA=" }, + "pause": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/pause/-/pause-0.0.1.tgz", + "integrity": "sha1-HUCLP9t2kjuVQ9lvtMnf1TXZy10=" + }, "performance-now": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz", diff --git a/server/package.json b/server/package.json index 11035c4..a16940c 100644 --- a/server/package.json +++ b/server/package.json @@ -13,12 +13,15 @@ "chai-http": "^4.3.0", "express": "^4.16.4", "jasmine": "^3.5.0", + "js-sha512": "^0.8.0", "knex": "^0.21.5", "mssql": "^6.2.1", "mysql": "^2.18.1", "mysql2": "^2.1.0", "nodemon": "^2.0.4", "oracledb": "^5.0.0", + "passport": "^0.4.1", + "passport-local": "^1.0.0", "pg": "^8.3.3", "sqlite3": "^5.0.0", "ts-node": "^8.10.2", diff --git a/server/test/integration/flows/AlbumFlow.js b/server/test/integration/flows/AlbumFlow.js index 2d76b99..719db34 100644 --- a/server/test/integration/flows/AlbumFlow.js +++ b/server/test/integration/flows/AlbumFlow.js @@ -4,92 +4,100 @@ const express = require('express'); import { SetupApp } from '../../../app'; import { expect } from 'chai'; import * as helpers from './helpers'; +import { sha512 } from 'js-sha512'; async function init() { chai.use(chaiHttp); const app = express(); - SetupApp(app, await helpers.initTestDB(), ''); - return app; + const knex = await helpers.initTestDB(); + + // Add test users. + await knex.insert({ email: "test1@test.com", passwordHash: sha512('pass1') }).into('users'); + await knex.insert({ email: "test2@test.com", passwordHash: sha512('pass2') }).into('users'); + + SetupApp(app, knex, ''); + + // Login as a test user. + var agent = chai.request.agent(app); + await agent + .post('/login?username=' + encodeURIComponent("test1@test.com") + '&password=' + encodeURIComponent('pass1')) + .send({}); + return agent; } describe('POST /album with no name', () => { - it('should fail', done => { - init().then((app) => { - chai - .request(app) - .post('/album') - .send({}) - .then((res) => { - expect(res).to.have.status(400); - done(); - }); - }); + it('should fail', async done => { + let agent = await init(); + let req = agent.keepOpen(); + try { + await helpers.createAlbum(req, {}, 400); + } finally { + req.close(); + agent.close(); + done(); + } }); }); describe('POST /album with a correct request', () => { - it('should succeed', done => { - init().then((app) => { - chai - .request(app) - .post('/album') - .send({ - name: "MyAlbum" - }) - .then((res) => { - expect(res).to.have.status(200); - expect(res.body).to.deep.equal({ - id: 1 - }); - done(); - }); - }); + it('should succeed', async done => { + let agent = await init(); + let req = agent.keepOpen(); + try { + await helpers.createAlbum(req, { name: "MyAlbum" }, 200, { id: 1 }); + } finally { + req.close(); + agent.close(); + done(); + } }); }); describe('PUT /album on nonexistent album', () => { - it('should fail', done => { - init().then((app) => { - chai - .request(app) - .put('/album/1') - .send({ - id: 1, - name: "NewAlbumName" - }) - .then((res) => { - expect(res).to.have.status(400); - done(); - }) - }) + it('should fail', async done => { + let agent = await init(); + let req = agent.keepOpen(); + try { + await helpers.modifyAlbum(req, 1, { id: 1, name: "NewAlbumName" }, 400); + } finally { + req.close(); + agent.close(); + done(); + } }); }); describe('PUT /album with an existing album', () => { - it('should succeed', done => { - init().then((app) => { - var req = chai.request(app).keepOpen(); - helpers.createAlbum(req, { name: "MyAlbum" }, 200, { id: 1 }) - .then(() => helpers.modifyAlbum(req, 1, { name: "MyNewAlbum" }, 200)) - .then(() => helpers.checkAlbum(req, 1, 200, { name: "MyNewAlbum", storeLinks: [], tagIds: [], songIds: [], artistIds: [] })) - .then(req.close) - .then(done); - }); + it('should succeed', async done => { + let agent = await init(); + let req = agent.keepOpen(); + try { + await helpers.createAlbum(req, { name: "MyAlbum" }, 200, { id: 1 }); + await helpers.modifyAlbum(req, 1, { name: "MyNewAlbum" }, 200); + await helpers.checkAlbum(req, 1, 200, { name: "MyNewAlbum", storeLinks: [], tagIds: [], songIds: [], artistIds: [] }); + } finally { + req.close(); + agent.close(); + done(); + } }); }); describe('POST /album with tags', () => { - it('should succeed', done => { - init().then((app) => { - var req = chai.request(app).keepOpen(); - helpers.createTag(req, { name: "Root" }, 200, { id: 1 }) - .then(() => helpers.createTag(req, { name: "Leaf", parentId: 1 }, 200, { id: 2 })) - .then(() => helpers.createAlbum(req, { name: "MyAlbum", tagIds: [ 1, 2 ] }, 200, { id: 1 })) - .then(() => helpers.checkAlbum(req, 1, 200, { name: "MyAlbum", storeLinks: [], tagIds: [ 1, 2 ], songIds: [], artistIds: [] })) - .then(req.close) - .then(done); - }); + it('should succeed', async done => { + let agent = await init(); + let req = agent.keepOpen(); + try { + await helpers.createTag(req, { name: "Root" }, 200, { id: 1 }) + await helpers.createTag(req, { name: "Leaf", parentId: 1 }, 200, { id: 2 }) + await helpers.createAlbum(req, { name: "MyAlbum", tagIds: [1, 2] }, 200, { id: 1 }) + await helpers.checkAlbum(req, 1, 200, { name: "MyAlbum", storeLinks: [], tagIds: [1, 2], songIds: [], artistIds: [] }) + } finally { + req.close(); + agent.close(); + done(); + } }); }); diff --git a/server/test/integration/flows/ArtistFlow.js b/server/test/integration/flows/ArtistFlow.js index 55a4e9b..04926eb 100644 --- a/server/test/integration/flows/ArtistFlow.js +++ b/server/test/integration/flows/ArtistFlow.js @@ -2,85 +2,101 @@ const chai = require('chai'); const chaiHttp = require('chai-http'); const express = require('express'); import { SetupApp } from '../../../app'; -import { expect } from 'chai'; import * as helpers from './helpers'; +import { sha512 } from 'js-sha512'; async function init() { chai.use(chaiHttp); const app = express(); - SetupApp(app, await helpers.initTestDB(), ''); - return app; + const knex = await helpers.initTestDB(); + + // Add test users. + await knex.insert({ email: "test1@test.com", passwordHash: sha512('pass1') }).into('users'); + await knex.insert({ email: "test2@test.com", passwordHash: sha512('pass2') }).into('users'); + + SetupApp(app, knex, ''); + + // Login as a test user. + var agent = chai.request.agent(app); + await agent + .post('/login?username=' + encodeURIComponent("test1@test.com") + '&password=' + encodeURIComponent('pass1')) + .send({}); + return agent; } describe('POST /artist with no name', () => { - it('should fail', done => { - init().then((app) => { - chai - .request(app) - .post('/artist') - .send({}) - .then((res) => { - expect(res).to.have.status(400); - done(); - }); - }); + it('should fail', async done => { + let agent = await init(); + var req = agent.keepOpen(); + try { + await helpers.createArtist(req, {}, 400); + } finally { + req.close(); + agent.close(); + done(); + } }); }); describe('POST /artist with a correct request', () => { - it('should succeed', done => { - init().then((app) => { - var req = chai.request(app).keepOpen(); - helpers.createArtist(req, { name: "MyArtist" }, 200, { id: 1 }) - .then(() => helpers.checkArtist(req, 1, 200, { name: "MyArtist", storeLinks: [], tagIds: [] })) - .then(req.close) - .then(done); - }); + it('should succeed', async done => { + let agent = await init(); + var req = agent.keepOpen(); + try { + await helpers.createArtist(req, { name: "MyArtist" }, 200, { id: 1 }); + await helpers.checkArtist(req, 1, 200, { name: "MyArtist", storeLinks: [], tagIds: [] }); + } finally { + req.close(); + agent.close(); + done(); + } }); }); describe('PUT /artist on nonexistent artist', () => { - it('should fail', done => { - init().then((app) => { - chai - .request(app) - .put('/artist/0') - .send({ - id: 0, - name: "NewArtistName" - }) - .then((res) => { - expect(res).to.have.status(400); - done(); - }) - }) + it('should fail', async done => { + let agent = await init(); + var req = agent.keepOpen(); + try { + await helpers.modifyArtist(req, 0, { id: 0, name: "NewArtistName" }, 400) + } finally { + req.close(); + agent.close(); + done(); + } }); }); describe('PUT /artist with an existing artist', () => { - it('should succeed', done => { - init().then((app) => { - var req = chai.request(app).keepOpen(); - helpers.createArtist(req, { name: "MyArtist" }, 200, { id: 1 }) - .then(() => helpers.modifyArtist(req, 1, { name: "MyNewArtist" }, 200)) - .then(() => helpers.checkArtist(req, 1, 200, { name: "MyNewArtist", storeLinks: [], tagIds: [] })) - .then(req.close) - .then(done); - }); + it('should succeed', async done => { + let agent = await init(); + var req = agent.keepOpen(); + try { + await helpers.createArtist(req, { name: "MyArtist" }, 200, { id: 1 }); + await helpers.modifyArtist(req, 1, { name: "MyNewArtist" }, 200); + await helpers.checkArtist(req, 1, 200, { name: "MyNewArtist", storeLinks: [], tagIds: [] }); + } finally { + req.close(); + agent.close(); + done(); + } }); }); describe('POST /artist with tags', () => { - it('should succeed', done => { - init().then((app) => { - var req = chai.request(app).keepOpen(); - helpers.createTag(req, { name: "Root" }, 200, { id: 1 }) - .then(() => helpers.createTag(req, { name: "Leaf", parentId: 1 }, 200, { id: 2 })) - .then(() => helpers.createArtist(req, { name: "MyArtist", tagIds: [ 1, 2 ] }, 200, { id: 1 })) - .then(() => helpers.checkArtist(req, 1, 200, { name: "MyArtist", storeLinks: [], tagIds: [ 1, 2 ] })) - .then(req.close) - .then(done); - }); + it('should succeed', async done => { + let agent = await init(); + var req = agent.keepOpen(); + try { + await helpers.createTag(req, { name: "Root" }, 200, { id: 1 }); + await helpers.createTag(req, { name: "Leaf", parentId: 1 }, 200, { id: 2 }); + await helpers.createArtist(req, { name: "MyArtist", tagIds: [1, 2] }, 200, { id: 1 }); + await helpers.checkArtist(req, 1, 200, { name: "MyArtist", storeLinks: [], tagIds: [1, 2] }); + } finally { + req.close(); + agent.close(); + done(); + } }); }); diff --git a/server/test/integration/flows/AuthFlow.js b/server/test/integration/flows/AuthFlow.js new file mode 100644 index 0000000..61f751d --- /dev/null +++ b/server/test/integration/flows/AuthFlow.js @@ -0,0 +1,145 @@ +const chai = require('chai'); +const chaiHttp = require('chai-http'); +const express = require('express'); +import { SetupApp } from '../../../app'; +import * as helpers from './helpers'; + +async function init() { + chai.use(chaiHttp); + const app = express(); + const knex = await helpers.initTestDB(); + + SetupApp(app, knex, ''); + + // Login as a test user. + var agent = chai.request.agent(app); + return agent; +} + +describe('Auth registration password and email constraints', () => { + it('are enforced', async done => { + let req = await init(); + try { + await helpers.createUser(req, "someone", "password1A!", 400); //no valid email + await helpers.createUser(req, "someone@email.com", "password1A", 400); //no special char + await helpers.createUser(req, "someone@email.com", "password1!", 400); //no capital letter + await helpers.createUser(req, "someone@email.com", "passwordA!", 400); //no number + await helpers.createUser(req, "someone@email.com", "Ϭassword1A!", 400); //non-ASCII in password + await helpers.createUser(req, "Ϭomeone@email.com", "password1A!", 400); //non-ASCII in email + await helpers.createUser(req, "someone@email.com", "pass1A!", 400); //password too short + await helpers.createUser(req, "someone@email.com", "password1A!", 200); + } finally { + req.close(); + done(); + } + }); +}); + +describe('Attempting to register an already registered user', () => { + it('should fail', async done => { + let req = await init(); + try { + await helpers.createUser(req, "someone@email.com", "password1A!", 200); + await helpers.createUser(req, "someone@email.com", "password1A!", 400); + } finally { + req.close(); + done(); + } + }); +}); + +describe('Auth login access for users', () => { + it('is correctly enforced', async done => { + let req = await init(); + try { + await helpers.createUser(req, "someone@email.com", "password1A!", 200); + await helpers.createUser(req, "someoneelse@other.com", "password2B!", 200); + await helpers.login(req, "someone@email.com", "password2B!", 401); + await helpers.login(req, "someoneelse@other.com", "password1A!", 401); + await helpers.login(req, "someone@email.com", "password1A!", 200); + await helpers.login(req, "someoneelse@other.com", "password2B!", 200); + } finally { + req.close(); + done(); + } + }); +}); + +describe('Auth access to objects', () => { + it('is only possible when logged in', async done => { + let req = await init(); + try { + await helpers.createUser(req, "someone@email.com", "password1A!", 200); + await helpers.login(req, "someone@email.com", "password1A!", 200); + + await helpers.createTag(req, { name: "Tag1" }, 200, { id: 1 }); + await helpers.createArtist(req, { name: "Artist1" }, 200, { id: 1} ); + await helpers.createAlbum(req, { name: "Album1" }, 200, { id: 1 }); + await helpers.createSong(req, { title: "Song1" }, 200, { id: 1 }); + + await helpers.checkTag(req, 1, 200); + await helpers.checkAlbum(req, 1, 200); + await helpers.checkArtist(req, 1, 200); + await helpers.checkSong(req, 1, 200); + + await helpers.logout(req, 200); + + await helpers.checkTag(req, 1, 401); + await helpers.checkAlbum(req, 1, 401); + await helpers.checkArtist(req, 1, 401); + await helpers.checkSong(req, 1, 401); + } finally { + req.close(); + done(); + } + }); +}); + +describe('Auth access to user objects', () => { + it('is restricted to each user', async done => { + let req = await init(); + try { + await helpers.createUser(req, "someone@email.com", "password1A!", 200); + await helpers.createUser(req, "someoneelse@other.com", "password2B!", 200); + + await helpers.login(req, "someone@email.com", "password1A!", 200); + await helpers.createTag(req, { name: "Tag1" }, 200, { id: 1 }); + await helpers.createArtist(req, { name: "Artist1" }, 200, { id: 1} ); + await helpers.createAlbum(req, { name: "Album1" }, 200, { id: 1 }); + await helpers.createSong(req, { title: "Song1" }, 200, { id: 1 }); + await helpers.logout(req, 200); + + await helpers.login(req, "someoneelse@other.com", "password2B!", 200); + await helpers.createTag(req, { name: "Tag2" }, 200, { id: 2 }); + await helpers.createArtist(req, { name: "Artist2" }, 200, { id: 2 } ); + await helpers.createAlbum(req, { name: "Album2" }, 200, { id: 2 }); + await helpers.createSong(req, { title: "Song2" }, 200, { id: 2 }); + await helpers.logout(req, 200); + + await helpers.login(req, "someone@email.com", "password1A!", 200); + await helpers.checkTag(req, 2, 404); + await helpers.checkAlbum(req, 2, 404); + await helpers.checkArtist(req, 2, 404); + await helpers.checkSong(req, 2, 404); + await helpers.checkTag(req, 1, 200); + await helpers.checkAlbum(req, 1, 200); + await helpers.checkArtist(req, 1, 200); + await helpers.checkSong(req, 1, 200); + await helpers.logout(req, 200); + + await helpers.login(req, "someoneelse@other.com", "password2B!", 200); + await helpers.checkTag(req, 1, 404); + await helpers.checkAlbum(req, 1, 404); + await helpers.checkArtist(req, 1, 404); + await helpers.checkSong(req, 1, 404); + await helpers.checkTag(req, 2, 200); + await helpers.checkAlbum(req, 2, 200); + await helpers.checkArtist(req, 2, 200); + await helpers.checkSong(req, 2, 200); + await helpers.logout(req, 200); + } finally { + req.close(); + done(); + } + }); +}); \ No newline at end of file diff --git a/server/test/integration/flows/QueryFlow.js b/server/test/integration/flows/QueryFlow.js index 242ba38..b71afd3 100644 --- a/server/test/integration/flows/QueryFlow.js +++ b/server/test/integration/flows/QueryFlow.js @@ -4,19 +4,32 @@ const express = require('express'); import { SetupApp } from '../../../app'; import { expect } from 'chai'; import * as helpers from './helpers'; +import { sha512 } from 'js-sha512'; async function init() { chai.use(chaiHttp); const app = express(); - SetupApp(app, await helpers.initTestDB(), '');; - return app; + const knex = await helpers.initTestDB(); + + // Add test users. + await knex.insert({ email: "test1@test.com", passwordHash: sha512('pass1') }).into('users'); + await knex.insert({ email: "test2@test.com", passwordHash: sha512('pass2') }).into('users'); + + SetupApp(app, knex, ''); + + // Login as a test user. + var agent = chai.request.agent(app); + await agent + .post('/login?username=' + encodeURIComponent("test1@test.com") + '&password=' + encodeURIComponent('pass1')) + .send({}); + return agent; } describe('POST /query with no songs', () => { - it('should give empty list', done => { - init().then((app) => { - chai - .request(app) + it('should give empty list', async done => { + let agent = await init(); + try { + let res = await agent .post('/query') .send({ 'query': {}, @@ -31,241 +44,243 @@ describe('POST /query with no songs', () => { 'ascending': true } }) + expect(res).to.have.status(200); + expect(res.body).to.deep.equal({ + songs: [], + tags: [], + artists: [], + albums: [], + }); + } finally { + agent.close(); + done(); + } + }); +}); + +describe('POST /query with several songs and filters', () => { + it('should give all correct results', async done => { + const song1 = { + songId: 1, + title: 'Song1', + storeLinks: [], + artists: [ + { + artistId: 1, + name: 'Artist1', + storeLinks: [], + } + ], + tags: [], + albums: [] + }; + const song2 = { + songId: 2, + title: 'Song2', + storeLinks: [], + artists: [ + { + artistId: 1, + name: 'Artist1', + storeLinks: [], + } + ], + tags: [], + albums: [] + }; + const song3 = { + songId: 3, + title: 'Song3', + storeLinks: [], + artists: [ + { + artistId: 2, + name: 'Artist2', + storeLinks: [], + } + ], + tags: [], + albums: [] + }; + + async function checkAllSongs(req) { + await req + .post('/query') + .send({ + "query": {}, + 'offsetsLimits': { + 'songOffset': 0, + 'songLimit': 10, + }, + 'ordering': { + 'orderBy': { + 'type': 0, + }, + 'ascending': true + } + }) .then((res) => { expect(res).to.have.status(200); expect(res.body).to.deep.equal({ - songs: [], - tags: [], + songs: [song1, song2, song3], artists: [], + tags: [], albums: [], }); - done(); }); - }) - }); -}); + } -describe('POST /query with several songs and filters', () => { - it('should give all correct results', done => { - init().then((app) => { - const song1 = { - songId: 1, - title: 'Song1', - storeLinks: [], - artists: [ - { - artistId: 1, - name: 'Artist1', - storeLinks: [], - } - ], - tags: [], - albums: [] - }; - const song2 = { - songId: 2, - title: 'Song2', - storeLinks: [], - artists: [ - { - artistId: 1, - name: 'Artist1', - storeLinks: [], - } - ], - tags: [], - albums: [] - }; - const song3 = { - songId: 3, - title: 'Song3', - storeLinks: [], - artists: [ - { - artistId: 2, - name: 'Artist2', - storeLinks: [], - } - ], - tags: [], - albums: [] - }; - - async function checkAllSongs(req) { - await req - .post('/query') - .send({ - "query": {}, - 'offsetsLimits': { - 'songOffset': 0, - 'songLimit': 10, + async function checkIdIn(req) { + await req + .post('/query') + .send({ + "query": { + "prop": "songId", + "propOperator": "IN", + "propOperand": [1, 3, 5] + }, + 'offsetsLimits': { + 'songOffset': 0, + 'songLimit': 10, + }, + 'ordering': { + 'orderBy': { + 'type': 0, }, - 'ordering': { - 'orderBy': { - 'type': 0, - }, - 'ascending': true - } - }) - .then((res) => { - expect(res).to.have.status(200); - expect(res.body).to.deep.equal({ - songs: [ song1, song2, song3 ], - artists: [], - tags: [], - albums: [], - }); + 'ascending': true + } + }) + .then((res) => { + expect(res).to.have.status(200); + expect(res.body).to.deep.equal({ + songs: [song1, song3], + artists: [], + tags: [], + albums: [], }); - } + }); + } - async function checkIdIn(req) { - await req - .post('/query') - .send({ - "query": { - "prop": "songId", - "propOperator": "IN", - "propOperand": [1, 3, 5] - }, - 'offsetsLimits': { - 'songOffset': 0, - 'songLimit': 10, + async function checkIdNotIn(req) { + await req + .post('/query') + .send({ + "query": { + "prop": "songId", + "propOperator": "NOTIN", + "propOperand": [1, 3, 5] + }, + 'offsetsLimits': { + 'songOffset': 0, + 'songLimit': 10, + }, + 'ordering': { + 'orderBy': { + 'type': 0, }, - 'ordering': { - 'orderBy': { - 'type': 0, - }, - 'ascending': true - } - }) - .then((res) => { - expect(res).to.have.status(200); - expect(res.body).to.deep.equal({ - songs: [ song1, song3 ], - artists: [], - tags: [], - albums: [], - }); + 'ascending': true + } + }) + .then((res) => { + expect(res).to.have.status(200); + expect(res.body).to.deep.equal({ + songs: [song2], + artists: [], + tags: [], + albums: [], }); - } + }); + } - async function checkIdNotIn(req) { - await req - .post('/query') - .send({ - "query": { - "prop": "songId", - "propOperator": "NOTIN", - "propOperand": [1, 3, 5] - }, - 'offsetsLimits': { - 'songOffset': 0, - 'songLimit': 10, + async function checkArtistIdIn(req) { + await req + .post('/query') + .send({ + "query": { + "prop": "artistId", + "propOperator": "IN", + "propOperand": [1] + }, + 'offsetsLimits': { + 'songOffset': 0, + 'songLimit': 10, + }, + 'ordering': { + 'orderBy': { + 'type': 0, }, - 'ordering': { - 'orderBy': { - 'type': 0, - }, - 'ascending': true - } - }) - .then((res) => { - expect(res).to.have.status(200); - expect(res.body).to.deep.equal({ - songs: [ song2 ], - artists: [], - tags: [], - albums: [], - }); + 'ascending': true + } + }) + .then((res) => { + expect(res).to.have.status(200); + expect(res.body).to.deep.equal({ + songs: [song1, song2], + artists: [], + tags: [], + albums: [], }); - } + }); + } - async function checkArtistIdIn(req) { - await req - .post('/query') - .send({ - "query": { - "prop": "artistId", - "propOperator": "IN", - "propOperand": [1] - }, - 'offsetsLimits': { - 'songOffset': 0, - 'songLimit': 10, - }, - 'ordering': { - 'orderBy': { - 'type': 0, + async function checkOrRelation(req) { + await req + .post('/query') + .send({ + "query": { + "childrenOperator": "OR", + "children": [ + { + "prop": "artistId", + "propOperator": "IN", + "propOperand": [2] }, - 'ascending': true - } - }) - .then((res) => { - expect(res).to.have.status(200); - expect(res.body).to.deep.equal({ - songs: [ song1, song2 ], - artists: [], - tags: [], - albums: [], - }); - }); - } - - async function checkOrRelation(req) { - await req - .post('/query') - .send({ - "query": { - "childrenOperator": "OR", - "children": [ - { - "prop": "artistId", - "propOperator": "IN", - "propOperand": [2] - }, - { - "prop": "songId", - "propOperator": "EQ", - "propOperand": 1 - } - ] - }, - 'offsetsLimits': { - 'songOffset': 0, - 'songLimit': 10, + { + "prop": "songId", + "propOperator": "EQ", + "propOperand": 1 + } + ] + }, + 'offsetsLimits': { + 'songOffset': 0, + 'songLimit': 10, + }, + 'ordering': { + 'orderBy': { + 'type': 0, }, - 'ordering': { - 'orderBy': { - 'type': 0, - }, - 'ascending': true - } - }) - .then((res) => { - expect(res).to.have.status(200); - expect(res.body).to.deep.equal({ - songs: [ song1, song3 ], - artists: [], - tags: [], - albums: [], - }); + 'ascending': true + } + }) + .then((res) => { + expect(res).to.have.status(200); + expect(res.body).to.deep.equal({ + songs: [song1, song3], + artists: [], + tags: [], + albums: [], }); - } - - var req = chai.request(app).keepOpen(); + }); + } - helpers.createArtist(req, { name: "Artist1" }, 200) - .then(() => helpers.createArtist(req, { name: "Artist2" }, 200)) - .then(() => helpers.createSong(req, { title: "Song1", artistIds: [1] }, 200)) - .then(() => helpers.createSong(req, { title: "Song2", artistIds: [1] }, 200)) - .then(() => helpers.createSong(req, { title: "Song3", artistIds: [2] }, 200)) - .then(() => checkAllSongs(req)) - .then(() => checkIdIn(req)) - .then(() => checkIdNotIn(req)) - .then(() => checkArtistIdIn(req)) - .then(() => checkOrRelation(req)) - .then(req.close) - .then(done) - }) + let agent = await init(); + let req = agent.keepOpen(); + try { + await helpers.createArtist(req, { name: "Artist1" }, 200); + await helpers.createArtist(req, { name: "Artist2" }, 200); + await helpers.createSong(req, { title: "Song1", artistIds: [1] }, 200); + await helpers.createSong(req, { title: "Song2", artistIds: [1] }, 200); + await helpers.createSong(req, { title: "Song3", artistIds: [2] }, 200); + await checkAllSongs(req); + await checkIdIn(req); + await checkIdNotIn(req); + await checkArtistIdIn(req); + await checkOrRelation(req); + } finally { + req.close(); + agent.close(); + done(); + } }); }); \ No newline at end of file diff --git a/server/test/integration/flows/SongFlow.js b/server/test/integration/flows/SongFlow.js index 9bd5234..00b7a0b 100644 --- a/server/test/integration/flows/SongFlow.js +++ b/server/test/integration/flows/SongFlow.js @@ -4,114 +4,128 @@ const express = require('express'); import { SetupApp } from '../../../app'; import { expect } from 'chai'; import * as helpers from './helpers'; +import { sha512 } from 'js-sha512'; async function init() { chai.use(chaiHttp); const app = express(); - SetupApp(app, await helpers.initTestDB(), ''); - return app; + const knex = await helpers.initTestDB(); + + // Add test users. + await knex.insert({ email: "test1@test.com", passwordHash: sha512('pass1') }).into('users'); + await knex.insert({ email: "test2@test.com", passwordHash: sha512('pass2') }).into('users'); + + SetupApp(app, knex, ''); + + // Login as a test user. + var agent = chai.request.agent(app); + await agent + .post('/login?username=' + encodeURIComponent("test1@test.com") + '&password=' + encodeURIComponent('pass1')) + .send({}); + return agent; } describe('POST /song with no title', () => { - it('should fail', done => { - init().then((app) => { - chai - .request(app) - .post('/song') - .send({}) - .then((res) => { - expect(res).to.have.status(400); - done(); - }); - }) + it('should fail', async done => { + let agent = await init(); + let req = agent.keepOpen(); + try { + await helpers.createSong(req, {}, 400); + } finally { + req.close(); + agent.close(); + done(); + } }); }); describe('POST /song with only a title', () => { - it('should return the first available id', done => { - init().then(async(app) => { - chai - .request(app) - .post('/song') - .send({ - title: "MySong" - }) - .then((res) => { - expect(res).to.have.status(200); - expect(res.body).to.deep.equal({ - id: 1 - }); - done(); - }); - }) + it('should return the first available id', async done => { + let agent = await init(); + let req = agent.keepOpen(); + try { + await helpers.createSong(req, { title: "MySong" }, 200, { id: 1 }); + } finally { + req.close(); + agent.close(); + done(); + } }); }); describe('POST /song with a nonexistent artist Id', () => { - it('should fail', done => { - init().then(async (app) => { - chai - .request(app) - .post('/song') - .send({ - title: "MySong", - artistIds: [1] - }) - .then((res) => { - expect(res).to.have.status(400); - done(); - }); - }) + it('should fail', async done => { + let agent = await init(); + let req = agent.keepOpen(); + try { + await helpers.createSong(req, { title: "MySong", artistIds: [1] }, 400); + } finally { + req.close(); + agent.close(); + done(); + } }); }); describe('POST /song with an existing artist Id', () => { - it('should succeed', done => { - init().then((app) => { - var req = chai.request(app).keepOpen(); - helpers.createArtist(req, { name: "MyArtist" }, 200, { id: 1 }) - .then(() => helpers.createSong(req, { title: "MySong", artistIds: [ 1 ] }, 200, { id: 1 }) ) - .then(req.close) - .then(done); - }); + it('should succeed', async done => { + let agent = await init(); + let req = agent.keepOpen(); + try { + await helpers.createArtist(req, { name: "MyArtist" }, 200, { id: 1 }); + await helpers.createSong(req, { title: "MySong", artistIds: [1] }, 200, { id: 1 }); + } finally { + req.close(); + agent.close(); + done(); + } }); }); describe('POST /song with two existing artist Ids', () => { - it('should succeed', done => { - init().then((app) => { - var req = chai.request(app).keepOpen(); - helpers.createArtist(req, { name: "Artist1" }, 200, { id: 1 }) - .then(() => helpers.createArtist(req, { name: "Artist2" }, 200, { id: 2 }) ) - .then(() => helpers.createSong(req, { title: "MySong", artistIds: [1, 2] }, 200, { id: 1 }) ) - .then(req.close) - .then(done); - }); + it('should succeed', async done => { + let agent = await init(); + let req = agent.keepOpen(); + try { + await helpers.createArtist(req, { name: "Artist1" }, 200, { id: 1 }) + await helpers.createArtist(req, { name: "Artist2" }, 200, { id: 2 }) + await helpers.createSong(req, { title: "MySong", artistIds: [1, 2] }, 200, { id: 1 }) + } finally { + req.close(); + agent.close(); + done(); + } }); }); describe('POST /song with an existent and a nonexistent artist Id', () => { - it('should fail', done => { - init().then((app) => { - var req = chai.request(app).keepOpen(); - helpers.createArtist(req, { name: "Artist1" }, 200, { id: 1 }) - .then(() => helpers.createSong(req, { title: "MySong", artistIds: [1, 2] }, 400) ) - .then(req.close) - .then(done); - }); + it('should fail', async done => { + let agent = await init(); + let req = agent.keepOpen(); + try { + await helpers.createArtist(req, { name: "Artist1" }, 200, { id: 1 }) + await helpers.createSong(req, { title: "MySong", artistIds: [1, 2] }, 400) + } finally { + req.close(); + agent.close(); + done(); + } }); }); describe('POST /song with tags', () => { - it('should succeed', done => { - init().then((app) => { - var req = chai.request(app).keepOpen(); - helpers.createTag(req, { name: "Root" }, 200, { id: 1 }) - .then(() => helpers.createTag(req, { name: "Leaf", parentId: 1 }, 200, { id: 2 })) - .then(() => helpers.createSong(req, { title: "Song", tagIds: [ 1, 2 ] }, 200, { id: 1 })) - .then(() => helpers.checkSong(req, 1, 200, { title: "Song", storeLinks: [], tagIds: [ 1, 2 ], albumIds: [], artistIds: [] })) - .then(req.close) - .then(done); - }); + it('should succeed', async done => { + let agent = await init(); + let req = agent.keepOpen(); + try { + await helpers.createTag(req, { name: "Root" }, 200, { id: 1 }) + await helpers.createTag(req, { name: "Leaf", parentId: 1 }, 200, { id: 2 }) + await helpers.createSong(req, { title: "Song", tagIds: [1, 2] }, 200, { id: 1 }) + await helpers.checkSong(req, 1, 200, { title: "Song", storeLinks: [], tagIds: [1, 2], albumIds: [], artistIds: [] }) + } finally { + req.close(); + agent.close(); + done(); + } }); }); diff --git a/server/test/integration/flows/TagFlow.js b/server/test/integration/flows/TagFlow.js index 7209f8b..c0f1b29 100644 --- a/server/test/integration/flows/TagFlow.js +++ b/server/test/integration/flows/TagFlow.js @@ -4,72 +4,84 @@ const express = require('express'); import { SetupApp } from '../../../app'; import { expect } from 'chai'; import * as helpers from './helpers'; +import { sha512 } from 'js-sha512'; async function init() { chai.use(chaiHttp); const app = express(); - SetupApp(app, await helpers.initTestDB(), ''); - return app; + const knex = await helpers.initTestDB(); + + // Add test users. + await knex.insert({ email: "test1@test.com", passwordHash: sha512('pass1') }).into('users'); + await knex.insert({ email: "test2@test.com", passwordHash: sha512('pass2') }).into('users'); + + SetupApp(app, knex, ''); + + // Login as a test user. + var agent = chai.request.agent(app); + await agent + .post('/login?username=' + encodeURIComponent("test1@test.com") + '&password=' + encodeURIComponent('pass1')) + .send({}); + return agent; } describe('POST /tag with no name', () => { - it('should fail', done => { - init().then((app) => { - chai - .request(app) - .post('/tag') - .send({}) - .then((res) => { - expect(res).to.have.status(400); - done(); - }); - }); + it('should fail', async done => { + let agent = await init(); + let req = agent.keepOpen(); + try { + await helpers.createTag(req, {}, 400); + } finally { + req.close(); + agent.close(); + done(); + } }); }); describe('POST /tag with a correct request', () => { - it('should succeed', done => { - init().then((app) => { - chai - .request(app) - .post('/tag') - .send({ - name: "MyTag" - }) - .then((res) => { - expect(res).to.have.status(200); - expect(res.body).to.deep.equal({ - id: 1 - }); - done(); - }); - }); + it('should succeed', async done => { + let agent = await init(); + let req = agent.keepOpen(); + try { + await helpers.createTag(req, { name: "MyTag" }, 200, { id: 1 }); + } finally { + req.close(); + agent.close(); + done(); + } }); }); describe('POST /tag with a parent', () => { - it('should succeed', done => { - init().then((app) => { - var req = chai.request(app).keepOpen(); - helpers.createTag(req, { name: "Tag1" }, 200, { id: 1 }) - .then(() => helpers.createTag(req, { name: "Tag2", parentId: 1 }, 200, { id: 2 }) ) - .then(() => helpers.checkTag(req, 2, 200, { name: "Tag2", parentId: 1 })) - .then(req.close) - .then(done); - }); + it('should succeed', async done => { + let agent = await init(); + let req = agent.keepOpen(); + try { + await helpers.createTag(req, { name: "Tag1" }, 200, { id: 1 }) + await helpers.createTag(req, { name: "Tag2", parentId: 1 }, 200, { id: 2 }) + await helpers.checkTag(req, 2, 200, { name: "Tag2", parentId: 1 }) + } finally { + req.close(); + agent.close(); + done(); + } }); }); describe('PUT /tag with a new parent', () => { - it('should succeed', done => { - init().then((app) => { - var req = chai.request(app).keepOpen(); - helpers.createTag(req, { name: "Tag1" }, 200, { id: 1 }) - .then(() => helpers.createTag(req, { name: "Tag2" }, 200, { id: 2 }) ) - .then(() => helpers.modifyTag(req, 2, { parentId: 1 }, 200) ) - .then(() => helpers.checkTag(req, 2, 200, { name: "Tag2", parentId: 1 })) - .then(req.close) - .then(done); - }); + it('should succeed', async done => { + let agent = await init(); + let req = agent.keepOpen(); + try { + await helpers.createTag(req, { name: "Tag1" }, 200, { id: 1 }) + await helpers.createTag(req, { name: "Tag2" }, 200, { id: 2 }) + await helpers.modifyTag(req, 2, { parentId: 1 }, 200) + await helpers.checkTag(req, 2, 200, { name: "Tag2", parentId: 1 }) + } finally { + req.close(); + agent.close(); + done(); + } }); }); \ No newline at end of file diff --git a/server/test/integration/flows/helpers.js b/server/test/integration/flows/helpers.js index 6d2da62..bbf427e 100644 --- a/server/test/integration/flows/helpers.js +++ b/server/test/integration/flows/helpers.js @@ -1,4 +1,5 @@ import { expect } from "chai"; +import { sha512 } from "js-sha512"; export async function initTestDB() { // Allow different database configs - but fall back to SQLite in memory if necessary. @@ -11,6 +12,7 @@ export async function initTestDB() { // Undoing and doing the migrations is a test in itself. await knex.migrate.rollback(undefined, true); await knex.migrate.latest(); + return knex; } @@ -184,4 +186,47 @@ export async function checkAlbum( expectStatus && expect(res).to.have.status(expectStatus); expectResponse && expect(res.body).to.deep.equal(expectResponse); }) +} + +export async function createUser( + req, + email, + password, + expectStatus = undefined, + expectResponse = undefined, +) { + const res = await req + .post('/register') + .send({ + email: email, + password: password, + }); + expectStatus && expect(res).to.have.status(expectStatus); + expectResponse && expect(res.body).to.deep.equal(expectResponse); +} + +export async function login( + req, + email, + password, + expectStatus = undefined, + expectResponse = undefined, +) { + const res = await req + .post('/login?username=' + encodeURIComponent(email) + '&password=' + encodeURIComponent(password)) + .send({}); + expectStatus && expect(res).to.have.status(expectStatus); + expectResponse && expect(res.body).to.deep.equal(expectResponse); +} + +export async function logout( + req, + expectStatus = undefined, + expectResponse = undefined, +) { + const res = await req + .post('/logout') + .send({}); + expectStatus && expect(res).to.have.status(expectStatus); + expectResponse && expect(res.body).to.deep.equal(expectResponse); } \ No newline at end of file